C++浓缩学习笔记(3)-面向对象

 文章主要引用:

阿秀的学习笔记 (interviewguide.cn)

牛客网 - 找工作神器|笔试题库|面试经验|实习招聘内推,求职就业一站解决_牛客网 (nowcoder.com)

一、面向对象

简述一下什么是面向对象(腾讯)

参考回答

  1. 面向对象是一种编程思想,把一切东西看成是一个个对象,比如人、耳机、鼠标、水杯等,他们各自都有属性,比如:耳机是白色的,鼠标是黑色的,水杯是圆柱形的等等,把这些对象拥有的属性变量和操作这些属性变量的函数打包成一个类来表示

  2. 面向过程和面向对象的区别

    面向过程:根据业务逻辑从上到下写代码,按照功能划分模块,各个模块相对独立,模块化实现的具体方法是使用子程序。但其数据和处理过程为相互独立的实体,当数据改变时,所有相关处理都要进行相应改变,程序可用性差。

    面向对象:将数据与函数绑定到一起,进行封装,这样能够更快速的开发程序,减少了重复代码的重写过程

简述一下面向对象的三大特征

参考回答

面向对象的三大特征是封装、继承、多态。

  1. 封装:将数据操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行 交互。封装本质上是一种管理:我们如何管理兵马俑呢?比如如果什么都不管,兵马俑就被随意破坏了。那么我们首先建了一座房子把兵马俑给封装起来。但是我们目的全封装起来,不让别人看。所以我们开放了售票通 道,可以买票突破封装在合理的监管机制下进去参观。类也是一样,不想给别人看到的,我们使用protected/private把成员封装起来。开放一些共有的成员函数对成员合理的访问。所以封装本质是一种管理。

  2. 继承:可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展

    扫描二维码关注公众号,回复: 17057921 查看本文章

    三种继承方式

    继承方式 private继承 protected继承 public继承
    基类的private成员 不可见 不可见 不可见
    基类的protected成员 变为private成员 仍为protected成员 仍为protected成员
    基类的public成员 变为private成员 变为protected成员 仍为public成员仍为public成员
  3. 多态:包含静态多态(函数重载)动态多态(子类重写父类虚函数,也只能是虚函数),一般指动态多态。多态是实现功能的复用,本质也归结到代码的复用。

二、继承

继承与派生

三种继承方式:public、private、protected. 

类型兼容规则:在需要基类对象的任何地方,都可以用公有派生类的对象来替代。(公有派生类得到了基类中除构造函数、析构函数之外的所有成员,具备基类所有功能,凡是基类能解决的,公有派生类都可以解决。)

  1. 派生类对象可以隐含转换为基类对象。

  2. 派生类对象可以初始化基类的引用。

  3. 派生类的指针可以隐含转换为基类的指针。

注意:替代后,派生类对象只能使用从基类继承的成员,仅仅发挥基类的作用。 P264

也可通过作用域标识符唯一标识派生类中所继承的成员,达到访问目的,解决了成员被隐藏问题。

class Derived: public Base1, public Base2 {}  //
//假设基类和派生类中都有fun();
Derived d;
d.fun();              //访问Derived的fun()
d.Base1::fun();       //访问Base1的fun()
d.Base2::fun();       //访问Base2的fun()
虚基类:

共同基类设置为虚基类,这时从不同路径继承来的同名数据成员内存中就只有一个副本,同一个函数也只有一个映射

class Base0 {int var0};
class Base1: virtual public Baes0 {int var1}
class Base2: virtual public Baes0 {int var2}
class Derived : public Base1, public Base2 {int var};
Derived d;
d.var0 = 2;     //直接访问虚基类的数据成员。

组合与继承的区别???

组合:整体-部分 关系。比如汽车(整体),由轮子和发动机等(部分)功能组成,可以移动。

继承:特殊-一般 关系。比如车可以跑,载人,而派生为卡车,具有车的一般功能,还可以拉货。

说说类继承时,派生类对不同关键字修饰的基类方法的访问权限

C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。

关键字 权限
public 可以被任意实体访问
protected 只允许子类及本类的成员函数访问
private 只允许本类的成员函数访问

类中的成员可以分为三种类型,分别为public成员、protected成员、public成员。类中可以直接访问自己类的public、protected、private成员,但类对象只能访问自己类的public成员。

  1. public继承:派生类可以访问基类的public、protected成员,不可以访问基类的private成员; 派生类对象可以访问基类的public成员,不可以访问基类的protected、private成员。

  2. protected继承:派生类可以访问基类的public、protected成员,不可以访问基类的private成员; 派生类对象不可以访问基类的public、protected、private成员。

  3. private继承:派生类可以访问基类的public、protected成员,不可以访问基类的private成员; 派生类对象不可以访问基类的public、protected、private成员。

private,protected,public 使用及区别

public访问权限:一个类的public成员变量、成员函数,可以通过类的成员函数、类的实例变量进行访问

protected访问权限:一个类的protected成员变量、成员函数,无法通过类的实例变量进行访问。但是可以通过类的友元函数、友元类进行访问。

private访问权限:一个类的private成员变量、成员函数,无法通过类的实例变量进行访问。但是可以通过类的友元函数、友元类进行访问。

通过public继承,所有基类成员(除了private),public、protected都到了派生类里面,public筛眼比较大,不会改变访问权限。
​
通过protected继承,所有基类成员(除了private),public、protected都到了派生类里面,protected筛眼大小适中,所有过来的成员都变成了protected。
​
通过private继承,所有基类成员(除了private),public、protected都到了派生类里面,private筛眼最小,所有过来的成员都变成了private。

private继承一次就结束了,而protected却可以一直继承;

继承关系中,子类对象和父类对象能否相互赋值?

(1)子类对象中既包含父类中继承来的变量,还包括自身所特有的.当把子类对象去赋值给父类对象时,把两者共有的部分进行了赋值.(向上)

(2)反之,父类对象赋值给子类对象时,由于父类对象不能够提供子类对象所特有的变量,因此会报错.(向下)

(3)对象指针也有这样的使用规则,子类指针可以直接赋值给父类指针.而将父类指针赋值给子类时,需要使用显示类型转换.

例如: 定义一个基类parent, 子类child继承parent,进行如下操作:

Parent = Child; Child = Parent; 请问Child 和原来的Child还一样吗?

Child = Parent; //会报错

里氏替换原则:

  1. 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法

  2. 子类中可以增加自己特性

  3. 类的方法重载父类的方法时,方法的前置条件(形参)要比父类方法的输入参数更宽松。

  4. 覆写或者实现父类的方法时输出结果(返回值)可以被缩小。

 多继承存在什么问题?

  1. 增加程序的复杂度,使得程序的编写和维护比较困难,容易出错;

  2. 在继承时,基类之间或基类与派生类之间发生成员同名时,将出现对成员访问的不确定性,即同名二义性

    解决:(1)利用作用域标识符::,作用域标识符唯一标识派生类中所继承的成员,达到访问目的;

    ​ (2)在派生类中定义同名成员,覆盖基类中的相关成员;

  3. 当派生类从多个基类派生,而这些基类又从同一个基类派生,则在访问此共同基类的成员时,将产生另一种不确定性,即路径二义性

    解决:使用虚继承(虚基类),使得不同路径继承来的同名成员在内存中只有一份拷贝。

虚基类: 被继承的类前面加上virtual关键字,将共同基类设置为虚基类,这时从不同路径继承来的同名数据成员在内存中就只有一个副本,同一个函数也只有一个映射。虚基类可以被实例化。

补充:用解决同名二义性 的方法也可以解决,只不过同名数据成员有好几个副本。

说说什么是虚继承,解决什么问题,如何实现?

虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。这将存在两个问题:其一,浪费存储空间;第二,存在二义性问题,通常可以将派生类对象的地址赋值给基类对象,实现的具体方式是,将基类指针指向继承类(继承类有基类的拷贝)中的基类对象的地址,但是多重继承可能存在一个基类的多份拷贝,这就出现了二义性。虚继承可以解决多种继承前面提到的两个问题

#include<iostream>
using namespace std;
class A{
public:
    int _a;
};
class B :virtual public A
{
public:
    int _b;
};
class C :virtual public A
{
public:
    int _c;
};
class D :public B, public C
{
public:
    int _d;
};
//菱形继承和菱形虚继承的对象模型
int main()
{
    D d;
    d.B::_a = 1;
    d.C::_a = 2;
    d._b = 3;
    d._c = 4;
    d._d = 5;
    cout << sizeof(D) << endl;
    return 0;
}



分别从菱形继承和虚继承来分析:

菱形继承中A在B,C,D,中各有一份,虚继承中,A共享。

上面的虚继承表实际上是一个指针数组。B、C实际上是虚基表指针,指向虚基表。

虚基表:存放相对偏移量,用来找虚基类

说说 C++ 中什么是菱形继承问题,如何解决

参考回答

  1. 下面的图表可以用来解释菱形继承问题。

  • 假设我们有类B和类C,它们都继承了相同的类A。另外我们还有类D,类D通过多重继承机制继承了类B和类C。因为上述图表的形状类似于菱形,因此这个问题被形象地称为菱形继承问题。现在,我们将上面的图表翻译成具体的代码:

    /*  *Animal类对应于图表的类A* */ class Animal { /* ... */ }; // 基类 {     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基类的两个子对象。

    所以,你会问Liger对象有两个Animal基类的子对象会出现什么问题?再看看上面的代码-调用"lg.getWeight()"将会导致一个编译错误。这是因为编译器并不知道是调用Tiger类的getWeight()还是调用Lion类的getWeight()。所以,调用getWeight方法是不明确的,因此不能通过编译。

  1. 我们给出了菱形继承问题的解释,但是现在我们要给出一个菱形继承问题的解决方案。如果Lion类和Tiger类在分别继承Animal类时都用virtual来标注,对于每一个Liger对象,C++会保证只有一个Animal类的子对象会被创建。看看下面的代码:

    class Tiger : virtual public Animal { /* ... */ }; class Lion : virtual public Animal { /* ... */ };
    
    
    
  • 你可以看出唯一的变化就是我们在类Tiger和类Lion的声明中增加了"virtual"关键字。现在类Liger对象将会只有一个Animal子对象,下面的代码编译正常:

    int main( ) {  Liger lg;  /*既然我们已经在Tiger和Lion类的定义中声明了"virtual"关键字,于是下面的代码编译OK */  int weight = lg.getWeight(); }
    

三、多态

RTTI是什么?

RTTI是运行阶段类型识别(Runtime Type Identification),C++引入这个机制是为了让程序在运行时能根据基类的指针或引用来获得该指针或引用所指的对象的实际类型。

RTTI即运行时类型识别,其功能由两个运算符实现:

  1. typeid运算符,用于返回表达式的类型,可以通过基类的指针获取派生类的数据类型;

  2. dynamic_cast运算符,具有类型检查的功能,用于将基类的指针或引用安全地转换成派生类的指针或引用。

RTTI只适用于包含虚函数的类。因为只有对于这种类层次结构,才应该将派生类的地址赋给基类指针。

简述一下 C++ 中的多态

狭义上: 多态分为静态多态动态多态

  1. 静态多态:编译器在编译期间完成的,编译器会根据实参类型来推断该调用哪个函数,如果有对应的函数,就调用,没有则在编译时报错。

    比如一个简单的加法函数:

    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;
    }
    
    
    
    

    显然,第一条语句会调用函数1,而第二条语句会调用函数2,这绝不是因为函数的声明顺序,不信你可以将顺序调过来试试。

  2. 动态多态:其实要实现动态多态,需要几个条件——即动态绑定条件:

    1. 虚函数。基类中必须有虚函数,在派生类中必须重写虚函数。

    2. 通过基类类型的指针或引用来调用虚函数。

    说到这,得插播一条概念:重写——也就是基类中有一个虚函数,而在派生类中也要重写一个原型(返回值、名字、参数)都相同的虚函数。不过协变例外。协变是重写的特例,基类中返回值是基类类型的引用或指针,在派生类中,返回值为派生类类型的引用或指针。

    //协变测试函数
    #include<iostream>
    using namespace std;
    
    class Base
    {
    public:
        virtual Base* FunTest()
        {
            cout << "victory" << endl;
            return this;
        }
    };
    
    class Derived :public Base
    {
    public:
        virtual Derived* FunTest()
        {
            cout << "yeah" << endl;
            return this;
        }
    };
    
    int main()
    {
        Base b;
        Derived d;
    
        b.FunTest();
        d.FunTest();
    
        return 0;
    }
    

广义上讲:多态性是指一段程序能够处理多种类型对象的能力。

强制多态、重载多态、包含多态、参数多态4种形式来实现。

  1. 强制多态: 通过数据类型转换(隐式或显示),将一种类型的数据转换成另一种类型的数据来实现。

  2. 重载多态: 函数重载,运算符重载。

  3. 包含多态: 采用虚函数实现包含多态。重写!

  4. 参数多态: 用模板来实现,分为函数模板,类模板。

    其中前两种是特殊多态性,是表面的多态(静态多态),后两种为一般多态性,是真正的多态性(动态多态)。

  5. 动态多态的必要条件:

    1. 需要有继承;

    2. 需要有虚函数覆盖;

    3. 需要有基类指针/引用指向子类对象;

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

参考回答

  1. 重写(动态多态)

    是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类对象调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。

    示例如下:

#include<bits/stdc++.h>  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类中重写 }
  1. 重载(静态多态)

    在C++中人们提出了用一个函数名定义多个函数,也就是所谓的函数重载。函数重载是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。

#include<bits/stdc++.h>  using namespace std;  class A {  void fun() {};  void fun(int i) {};  void fun(int i, int j) {};     void fun1(int i,int j){}; };

重写(晚绑定,动态多态)底层实现(百度)

虚函数的地址是运行时绑定的,而普通函数是编译时就确定了,对象调用的地址就已经确定了。

动态体现在通过指针一步一步找到实际的函数:一个类的成员函数加virtual,被声明为虚函数时,该类的对象就会增加一个虚函数指针(4/8字节),指向虚函数表(每个对象有自己的虚函数指针,虚函数表只有一个,所有的虚函数指针指向同一个虚函数表),虚函数表中保存了虚函数的地址,子类继承了这个虚函数表,重写了父类的虚函数,子类的虚函数表中的虚函数入口地址就修改为子类中虚函数入口地址(没有重写不变,还是指向父类的虚函数),现在一个父类的指针指向一个子类的对象(引用也可以,引用本质是指针常量),用父类指针调用被重写的虚函数时,根据指针可以找到这个子类的对象,根据子类对象中的虚函数指针找到虚函数表,虚函数表中这个虚函数

​ 静态/非静态函数,是不占用对象的内存的,只有非静态成员变量才占用对象的内存,类中有虚函数的时候,会发现类中多了4/8个字节,这4/8个字节就是虚函数指针(vptr),指向虚函数表。

虚函数按照其声明顺序存放于虚函数表中;
父类的虚函数存放在子类虚函数的前面;
多继承中,每个父类都有自己的虚函数表;
子类的成员函数被存放于第一个父类的虚函数表中;

虚函数表里存放的内容是什么时候写进去的?(百度)

参考回答

  1. 虚函数表是一个存储虚函数地址的数组,以NULL结尾。虚表(vftable)在编译阶段生成,对象内存空间开辟以后,写入对象中的 vfptr,然后调用构造函数。即:虚表在构造函数之前写入

  2. 除了在构造函数之前写入之外,我们还需要考虑到虚表的二次写入机制,通过此机制让每个对象的虚表指针都能准确的指向到自己类的虚表,为实现动态多态提供支持。在程序运行是选择合适的成员函数。

重载(早绑定,静态多态)底层实现(百度)

C++利用命名倾轧(name mangling)技术,来改名函数名,区分参数不同的同名函数。命名倾轧是在编译阶段完成的。

C++定义同名重载函数:

#include<iostream> using namespace std; int func(int a,double b) {  return ((a)+(b)); } int func(double a,float b) {  return ((a)+(b)); } int func(float a,int b) {  return ((a)+(b)); } int main() {  return 0; }

由上图可得,d代表double,f代表float,i代表int,加上参数首字母以区分同名函数。

说说 C 语言如何实现 C++ 语言中的重载

参考答案

c语言中不允许有同名函数,因为编译时函数命名是一样的,不像c++会添加参数类型和返回类型作为函数编译后的名称,进而实现重载。如果要用c语言显现函数重载,可通过以下方式来实现:

  1. 使用函数指针来实现,重载的函数不能使用同名称,只是类似的实现了函数重载功能

  2. 重载函数使用可变参数,方式如打开文件open函数

  3. gcc有内置函数,程序使用编译函数可以实现函数重载

示例如下:

#include<stdio.h>   void func_int(void * a) {     printf("%d\n",*(int*)a);  //输出int类型,注意 void * 转化为int }   void func_double(void * b) {     printf("%.2f\n",*(double*)b); }   typedef void (*ptr)(void *);  //typedef申明一个函数指针   void c_func(ptr p,void *param) {      p(param);                //调用对应函数 }   int main() {     int a = 23;     double b = 23.23;     c_func(func_int,&a);     c_func(func_double,&b);     return 0; }

虚函数的理解

虚函数是动态绑定的基础,虚函数必须是非静态的成员函数。 虚函数经过派生之后,在类族中就可以实现动态多态了。

函数重写(覆盖):基类声明虚函数,派生类声明一个同名,同参数,同返回值函数重写该虚函数,实现多态。

只有通过基类指针或引用调用虚函数时,才会发生动态绑定。这是因为:基类的指针可以指向派生类的对象,基类的引用可以作为派生类的别名,但基类的对象却不能表示派生类的对象

class Base1{
  virtual display();         //虚函数
}
class Base2: public Base1 {
  virtual display();         //覆盖基类虚函数
}
class Derived: public Base2{
  virtual display();        //覆盖基类虚函数
}
void fun (Base1* ptr) {ptr->display();}    //参数为基类指针
fun(&base1);     //base1 为Base1的对象
fun(&base2);     //base2 为Base2的对象
fun(&derived);   //derived为Derived的对象
结果:
Base1::display()
Base2::display()
Derived::display()

虚表:每个多态类有一个虚表(virtual table),虚表中有当前类的各个虚函数的入口地址,每个对象有一个指向当前类的虚表的指针(虚指针vptr),虚表的内容是编译器安排的。(优点:节省空间,每个对象只需保存虚指针就行。(指向虚表首地址的指针)) 每个对象的vptr的存放地址都不同,但都指向同一虚函数表。

动态绑定的实现P341: 首先,构造函数为对象的虚指针赋值。然后,通过多态类型的指针或引用调用成员函数时,通过虚指针找到虚表中所调用的虚函数的入口地址。最后,通过该入口地址调用虚函数。

补充:非静态成员函数、析构函数可以是虚函数,但不能声明虚构造函数。

虚函数指针放在类里的什么位置?64位机,类里面10个虚函数,sizeof(CLASS A)是多大

​ 虚指针存在于对象头部,大小8字节(64位)。编译器为了支持虚函数,会产生额外负担,指向虚表的虚指针(vptr)大小,64位机器上占8Byte,32位则是4字节。无论一个类有多少个虚函数,每个类对象都只有这一个指针。

sizeof(A的一个对象)= 非静态数据成员大小 + 虚指针大小(8字节) + 数据对齐额外空间;平时所声明的类只是一种类型定义,本身没有大小,使用sizeof运算符得到的是该类型对象的大小。

虚函数表不占用类对象的内存空间。

如果类A是一个空类,那么sizeof(A)的值为多少?

sizeof(A)的值为1,因为编译器需要区分这个空类的不同实例,分配一个字节,可以使这个空类的不同实例拥有独一无二的地址。

构造函数,析构函数能否为虚函数

构造函数不能定义为虚函数。

不能虚构造:

  1. 从存储空间角度:虚函数对应一个vtale,这个表的地址是存储在对象的内存空间的。如果将构造函数设置为虚函数,就需要到vtable 中调用,可是对象还没有实例化,没有内存空间分配,如何调用。(悖论)

  2. 从使用角度:虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。

  3. 从实现上看,vbtl 在构造函数调用后才建立,因而构造函数不可能成为虚函数。

虚函数的调用依赖于虚函数表,而虚指针vptr需要在构造函数中进行初始化,所以无法调用定义为虚函数的构造函数。

为什么基类的析构函数需要定义为虚函数?

虚析构:将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。如果基类的析构函数不是虚函数,在特定情况下会导致派生来无法被析构。

C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

  1. 用派生类类型指针绑定派生类实例,析构的时候,不管基类析构函数是不是虚函数,都会正常析构

  2. 用基类类型指针绑定派生类实例,析构的时候,如果基类析构函数不是虚函数,则只会析构基类,不会析构派生类对象,从而造成内存泄漏。为什么会出现这种现象呢,个人认为析构的时候如果没有虚函数的动态绑定功能,编译器实施静态绑定,就只根据指针的类型来进行的,而不是根据指针绑定的对象来进行,所以是调用了基类的析构函数;如果基类的析构函数是虚函数,则析构的时候就要根据指针绑定的对象来调用对应的析构函数了。

  3. (2的解释)定义一个基类的指针p,在delete p时,如果基类的析构函数是虚函数,这时只会看p所赋值的对象,如果p赋值的对象是派生类的对象,就会调用派生类的析构函数(毫无疑问,在这之前也会先调用基类的构造函数,在调用派生类的构造函数,然后调用派生类的析构函数,基类的析构函数,所谓先构造的后释放);如果p赋值的对象是基类的对象,就会调用基类的析构函数,这样就不会造成内存泄露。

    如果基类的析构函数不是虚函数,在delete p时,调用析构函数时,只会看指针的数据类型,而不会去看赋值的对象,这样就会造成内存泄露。

请问构造函数中的能不能调用虚方法

不可以,因为多态的vptr指针是分布初始化的,在子类进行初始化的时候先调用父类的构造函数,这时候子类的父类的Vptr指针都是指向父类的,因此产生不了多态。

简述一下虚函数和纯虚函数,以及实现原理

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

    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
    }
    
    
    
    

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

  2. 纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0” virtualvoid GetName() =0。在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。为了解决上述问题,将函数定义为纯虚函数,则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。将函数定义为纯虚函数能够说明,该函数为后代类型提供了可以覆盖的接口,但是这个类中的函数绝不会调用。声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。必须在继承类中重新声明函数(不要后面的=0)否则该派生类也不能实例化,而且它们在抽象类中往往没有定义。定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计

  3. 者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

//抽象类
class Person{
    public:
        //纯虚函数
        virtual void GetName()=0;
};
class Student:public Person{
    public:
        Student(){
        };
        void GetName(){
            cout<<"StudentName:xiaosi"<<endl;
        };
};
int main(){
    Student student;
}


如何理解抽象类?

参考回答

  1. 抽象类的定义如下:

    纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”,有虚函数的类就叫做抽象类。

  2. 抽象类有如下几个特点:

    1)抽象类只能用作其他类的基类,不能建立抽象类对象。

    2)抽象类不能用作参数类型、函数返回类型或显式转换的类型

    3)可以定义指向抽象类的指针和引用,此指针可以指向它的派生类,进而实现多态性。

纯虚函数的应用场景主要包括:

  • 设计模式:例如在模板方法模式中,基类定义一个算法的骨架,而将一些步骤延迟到子类中。这些需要在子类中实现的步骤就可以声明为纯虚函数。

  • 接口定义:可以创建一个只包含纯虚函数的抽象类作为接口。所有实现该接口的类都必须提供这些函数的实现。

纯虚函数与抽象类

纯虚函数:在基类中声明的虚函数,但没有函数的实现部分。各个派生类需根据实际需求给出各自定义。

virtual void display() const = 0;     //纯虚函数  即在一般虚函数原型上后面加“= 0”;

注意:纯虚函数不同于函数体为空的虚函数,纯虚函数根本没有函数体。前者所在的类是抽象类,不能直接进行实例化,而后者所在的类可以实例化。

带有纯虚函数的类就是抽象类,它为一个类族提供统一的操作界面(建立一个公共的接口)。建立抽象类就是为了通过它使用其中的成员函数,发挥多态特性。抽象类无法实例化,即:无法定义一个抽象类的对象,只能通过继承机制,生成抽象类的非抽象派生类,然后再实例化。

抽象类和接口区别

接口:接口是一个概念。它在C++中用抽象类来实现

区别:

  1. 抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。举个简单的例子,飞机和鸟是不同类的事物,但是它们都有一个共性,就是都会飞。那么在设计的时候,可以将飞机设计为一个类Airplane,将鸟设计为一个类Bird,但是不能将 飞行 这个特性也设计为类,因此它只是一个行为特性,并不是对一类事物的抽象描述。此时可以将 飞行 设计为一个接口Fly,包含方法fly( ),然后Airplane和Bird分别根据自己的需要实现Fly这个接口。然后至于有不同种类的飞机,比如战斗机、民用飞机等直接继承Airplane即可,对于鸟也是类似的,不同种类的鸟直接继承Bird类即可。从这里可以看出,继承是一个 "是不是"的关系,而 接口 实现则是 "有没有"的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系,比如鸟是否能飞(或者是否具备飞行这个特点),能飞行则可以实现这个接口,不能飞行就不实现这个接口。

  2. 设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范。比如某个电梯都装了某种报警器,一旦要更新报警器,就必须全部更新。也就是说对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动。

两者使用时机的区别

1.抽象类:设计模式,模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中某些步骤的具体实现。

2.接口:实现类需要具备很多不同的功能,但各个功能之间可能没有任何联系。

说说纯虚函数能实例化吗,为什么?派生类要实现吗,为什么?

参考回答

  1. 纯虚函数不可以实例化,但是可以用其派生类实例化,

  2. 虚函数的原理采用 vtable。类中含有纯虚函数时,其vtable 不完全,有个空位。

    即“纯虚函数在类的vftable表中对应的表项被赋值为0。也就是指向一个不存在的函数。由于编译器绝对不允许有调用一个不存在的函数的可能,所以该类不能生成对象。在它的派生类中,除非重写此函数,否则也不能生成对象。”

    所以纯虚函数不能实例化。

  3. 纯虚函数是在基类中声明的虚函数,它要求任何派生类都要定义自己的实现方法,以实现多态性。

  4. 定义纯虚函数是为了实现一个接口,用来规范派生类的行为,也即规范继承这个类的程序员必须实现这个函数。派生类仅仅只是继承函数的接口。纯虚函数的意义在于,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但基类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

说说C++中虚函数与纯虚函数的区别

参考回答

  1. 虚函数和纯虚函数可以定义在同一个类中,含有纯虚函数的类被称为抽象类,而只含有虚函数的类不能被称为抽象类。

  2. 虚函数可以被直接使用,也可以被子类重载以后,以多态的形式调用,而纯虚函数必须在子类中实现该函数才可以使用,因为纯虚函数在基类有声明而没有定义。

  3. 虚函数和纯虚函数都可以在子类中被重载,以多态的形式被调用。

  4. 虚函数和纯虚函数通常存在于抽象基类之中,被继承的子类重载,目的是提供一个统一的接口。

  5. 虚函数的定义形式:virtual{};纯虚函数的定义形式:virtual { } = 0;在虚函数和纯虚函数的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时要求前期绑定,然而虚函数却是动态绑定,而且被两者修饰的函数生命周期也不一样。

答案解析

  1. 我们举个虚函数的例子:

    class A { public:     
    virtual void foo()     
    {         cout<<"A::foo() is called"<<endl;     } }; 
    class B:public A { 
    public:     
    void foo()     {         cout<<"B::foo() is called"<<endl;     } }; 
    int main(void) {     
    A *a = new B();     
    a->foo();   // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!     
    return 0; }
    
    
    

    这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。它虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。 虚函数只能借助于指针或者引用来达到多态的效果。

  2. 纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”

    virtual void funtion1()=0

    为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。

    在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。

    为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。 声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。

    纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。

    定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。

    纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

C++ 中哪些函数不能被声明为虚函数?

参考回答

常见的不不能声明为虚函数的有:普通函数(非成员函数),静态成员函数,内联成员函数,构造函数,友元函数。

  1. 为什么C++不支持普通函数为虚函数?

​ 普通函数(非成员函数)只能被overload,不能被override,声明为虚函数也没有什么意思,因此编译器会在编译时绑定函数。

  1. 为什么C++不支持构造函数为虚函数?

    构造函数用来创建一个新的对象,而虚函数的运行是建立在对象的基础上(不完全了解细节的情况下也能正确处理对象,在构造函数执行时,对象尚未形成,所以不能将构造函数定义为虚函数

  2. 为什么C++不支持内联成员函数为虚函数?

    其实很简单,那内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。

    内联函数是在编译时期展开,而虚函数的特性是运行时才动态联编,所以两者矛盾,不能定义内联函数为虚函数

  3. 为什么C++不支持静态成员函数为虚函数?

    这也很简单,静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他也没有要动态绑定的必要性。

    静态成员函数属于一个类而非某一对象,没有this指针,它无法进行对象的判别

  4. 为什么C++不支持友元函数为虚函数?

    不是成员函数,就不能为虚函数。

友元函数

类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。

友元关系

  1. 友元关系是不能传递的; C是B的友元,B是A的友元,如果没有声明,C和A没有任何友元关系。

  2. 友元关系是单向的; B是A的友元, 但A不是B的友元。

  3. 友元关系是不被继承的;

四、类与对象

C++类对象,占用内存大小与什么有关,空类占多大空间。

平时所声明的类只是一种类型定义,本身没有大小,使用sizeof运算符得到的是该类型对象的大小。

  1. 非静态数据成员(例如char-1字节,int-4字节);

  2. 虚函数指针所占内存虚指针,64位-8字节,32位4字节)

  3. 数据对齐处理所占空间(按照成员变量中类型大小最大的来计算)-用空间效率来换时间效率。

编译器为了支持虚函数,会产生额外负担,指向虚表的虚指针(vptr)大小,64位机器上占8Byte,32位则是4字节。无论一个类有多少个虚函数,一个类对象都只有这一个指针。

注意:如果基类不为空类,则派生类的大小要加上基类所占空间大小。单一继承和多继承的空类空间大小是1字节,但虚继承涉及虚表(虚指针),大小为8字节。

成员函数、静态数据成员,构造函数、析构函数都不占对象的内存空间。

答:空类型对象不包含任何信息,大小却不是0。这是因为当声明该类型的对象的时候,它必须在内存中占有一定空间,否则无法使用。C++中每个空类型的实例占1Byte空间。

补充:函数代码是存储在对象空间之外的,而且,函数代码段是公用的,即如果对同一个类定义了10个对象,这些对象的成员函数对应的是同一个函数代码段,而不是10个不同的函数代码段。

class Base{
private:
     char c;
     int a;
public:
     virtual void func1();
};
​
class Child : public Base{
public:
     virtual void func2();
private:
     int b;
     //double b;
};
父类大小: char c(4,和int数据对齐),int a(4),虚指针(8) --大小16
子类大小: int b(4 + 4,和虚指针数据对齐),父类大小(16)--大小24   //数据对齐
//子类大小: double b(8),父类大小(16)--大小24
//虽然子类也有自己的虚函数,但是它继承了父类的虚函数表指针,也等于继承了父类的虚表和虚指针
//它自己的虚函数地址会加入到从父类继承过来的虚函数表中。

说说构造函数有几种,分别什么作用

参考答案

C++中的构造函数可以分为4类:默认构造函数、初始化构造函数、拷贝构造函数、移动构造函数。

  1. 默认构造函数和初始化构造函数。 在定义类的对象的时候,完成对象的初始化工作。

    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; }

    有了有参的构造了,编译器就不提供默认的构造函数。

  2. 拷贝构造函数

    #include "stdafx.h" 
    #include "iostream.h"  
    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; }

    复制本类的对象 赋值构造函数默认实现的是值拷贝(浅拷贝)。

  3. 移动构造函数。用于将其他类型的变量,隐式转换为本类对象。下面的转换构造函数,将int类型的r转换为Student类型的对象,对象的age为r,num为1004.

    Student(int r) {  int num=1004;  int age= r; }

拷贝初始化和直接初始化

  1. ClassTest ct1(“ab”); 这条语句属于直接初始化,直接调用构造函数;

  2. ClassTest ct2 = “ab”; 这条语句为复制初始化,它首先调用构造函数创建一个临时对象,然后调用拷贝构造函数,把这个临时对象作为参数,构造对象ct2;

  3. ClassTest ct3 = ct1;这条语句为复制初始化,因为 ct1 本来已经存在,所以不需要调用相关的构造函数,而直接调用复制构造函数,把它值复制给对象 ct3;

  4. ClassTest ct4(ct1);这条语句为直接初始化,因为 ct1 本来已经存在,直接调用复制构造函数,生成对象 ct3 的副本对象 ct4。

直接初始化:根据参数直接调用相应的构造函数;

拷贝初始化:构造函数创建一个临时对象,然后调用拷贝构造函数,把这个临时对象作为参数,构造对象

只定义析构函数,会自动生成哪些构造函数

参考答案

只定义了析构函数,编译器将自动为我们生成拷贝构造函数和默认构造函数

默认构造函数和初始化构造函数。 在定义类的对象的时候,完成对象的初始化工作。

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 "stdafx.h"
#include "iostream.h"
​
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;
}
​

赋值构造函数默认实现的是值拷贝(浅拷贝)。

答案解析

示例如下:

class HasPtr
{
public:
    HasPtr(const string& s = string()) :ps(new string(s)), i(0) {}
    ~HasPtr() { delete ps; }
private:
    string * ps;
    int i;
};



如果类外面有这样一个函数:

HasPtr f(HasPtr hp)
{
    HasPtr ret = hp;
    ///... 其他操作
    return ret;
 
}



当函数执行完了之后,将会调用hp和ret的析构函数,将hp和ret的成员ps给delete掉,但是由于ret和hp指向了同一个对象,因此该对象的ps成员被delete了两次,这样产生一个未定义的错误,所以说,如果一个类定义了析构函数,那么它要定义自己的拷贝构造函数和默认构造函数。

说说一个类,默认会生成哪些函数

参考答案

定义一个空类

class Empty
{
};



默认会生成以下几个函数

  1. 无参的构造函数

    在定义类的对象的时候,完成对象的初始化工作。

Empty()
{
}



  1. 拷贝构造函数

    拷贝构造函数用于复制本类的对象

Empty(const Empty& copy)
{
}



  1. 赋值运算符

Empty& operator = (const Empty& copy)
{
}



  1. 析构函数(非虚)

~Empty()
{
}


 构造函数初始化,函数体内初始化和成员列表初始化的区别

在 C++ 中,对象的成员变量可以通过不同的方式进行初始化,包括构造函数初始化、函数体内初始化和成员初始化列表。这些初始化方式有一些区别,以下是它们之间的比较:

  • 构造函数初始化: 在构造函数内部使用赋值语句进行初始化。这是最常见的初始化方式,它在对象创建时会调用构造函数,并在函数体内执行初始化操作。这种方式的初始化发生在对象的构造函数体内。

  • class MyClass {
    public:
        MyClass() {
            x = 0; // 构造函数初始化
        }
    private:
        int x;
    };
    
  • 函数体内初始化: 在构造函数内部,可以使用函数体内的赋值语句或初始化列表进行初始化。这种方式的初始化发生在构造函数体内,但在构造函数体的其他语句之后。

  • class MyClass {
    public:
        MyClass() {
            // 构造函数体内初始化
            x = 0;
        }
    private:
        int x;
    };
    
  • 成员初始化列表: 使用初始化列表在构造函数开始时初始化成员变量。这种方式的初始化发生在进入构造函数体之前,这有助于避免不必要的变量初始化和拷贝构造。

  • class MyClass {
    public:
        // 成员初始化列表初始化
        MyClass() : x(0) {
        }
    private:
        int x;
    };
    

    成员初始化列表通常被认为是更好的选择,因为它可以减少额外的构造和拷贝操作,提高性能。在某些情况下,如对于 const 成员变量和引用成员变量,使用成员初始化列表是必须的。

普通的函数和成员函数的区别?

普通函数是在类的外部定义的,而成员函数是在类的内部定义的。

普通函数不能直接访问类的私有(private)和保护(protected)成员,而成员函数可以访问类的所有成员,包括私有和保护成员。

普通函数可以直接调用,而成员函数需要通过类的对象来调用。

成员函数有一个特殊的指针this,它指向调用该成员函数的对象。普通函数没有这个指针。

this 指针是干嘛的?(腾讯)

this 指针是指向当前对象的地址。主要用于在类的成员函数中访问当前对象的成员变量和成员函数。

当一个对象调用自己的成员函数时,编译器会通过this隐式地将对象的地址传递给成员函数。通过this指针,成员函数可以访问和操作当前对象的成员变量和成员函数

this指针只能在非静态成员函数中使用,因为静态成员函数没有this指针,它们不属于任何具体的对象

说说 C++ 类对象的初始化顺序,有多重继承情况下的顺序

参考答案

  1. 创建派生类的对象,基类的构造函数优先被调用(也优先于派生类里的成员类);

  2. 如果类里面有成员类,成员类的构造函数优先被调用;(也优先于该类本身的构造函数)

  3. 基类构造函数如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序而不是它们在成员初始化表中的顺序;

  4. 成员类对象构造函数如果有多个成员类对象,则构造函数的调用顺序是对象在类中被声明的顺序而不是它们出现在成员初始化表中的顺序;

  5. 派生类构造函数,作为一般规则派生类构造函数应该不能直接向一个基类数据成员赋值而是把值传递给适当的基类构造函数,否则两个类的实现变成紧耦合的(tightly coupled)将更加难于正确地修改或扩展基类的实现。(基类设计者的责任是提供一组适当的基类构造函数)

  6. 综上可以得出,初始化顺序:

    父类构造函数–>成员类对象构造函数–>自身构造函数

    其中成员变量的初始化与声明顺序有关,构造函数的调用顺序是类派生列表中的顺序。

    析构顺序和构造顺序相反。

简述下向上转型和向下转型

  1. 子类转换为父类:向上转型,使用dynamic_cast<type_id>(expression),这种转换相对来说比较安全不会有数据的丢失;

  2. 父类转换为子类:向下转型,可以使用强制转换,这种转换时不安全的,会导致数据的丢失,原因是父类的指针或者引用的内存中可能不包含子类的成员的内存。

简述下深拷贝和浅拷贝,如何实现深拷贝

  1. 浅拷贝:又称值拷贝,将源对象的值拷贝到目标对象中去,本质上来说源对象和目标对象共用一份实体,只是所引用的变量名不同,地址其实还是相同的。举个简单的例子,你的小名叫西西,大名叫冬冬,当别人叫你西西或者冬冬的时候你都会答应,这两个名字虽然不相同,但是都指的是你。

  2. 深拷贝,拷贝的时候先开辟出和源对象大小一样的空间,然后将源对象里的内容拷贝到目标对象中去,这样两个指针就指向了不同的内存位置。并且里面的内容是一样的,这样不但达到了我们想要的目的,还不会出现问题,两个指针先后去调用析构函数,分别释放自己所指向的位置。即为每次增加一个指针,便申请一块新的内存,并让这个指针指向新的内存,深拷贝情况下,不会出现重复释放同一块内存的错误。

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

    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;
    }
    
    
    
    

    这里的拷贝构造函数我们很容易理解,先开辟出和源对象一样大的内存区域,然后将需要拷贝的数据复制到目标拷贝对象 , 那么这里的赋值运算符的重载是怎么样做的呢?

    这种方法解决了我们的指针悬挂问题,通过不断的开空间让不同的指针指向不同的内存,以防止同一块内存被释放两次的问题。

拷贝构造函数和赋值运算符重载之间有什么区别?

拷贝构造函数用于构造新的对象;

赋值运算符重载用于将原对象的内容拷贝到目标对象中,而且若目标对象中包含未释放的内存需要先将其释放;

构造函数和析构函数能抛出异常吗?

从语法的角度来说,构造函数可以抛出异常,但从逻辑和风险控制的角度来说,尽量不要抛出异常,否则可能导致内存泄漏。

析构函数不可以抛出异常,如果析构函数抛出异常,则异常点之后的程序,比如释放内存等操作,就不会被执行,从而造成内存泄露的问题;而且当异常发生时,C++通常会调用对象的构函数来释放资源,如果此时析构函数也抛出异常,即前一个异常未处理又出现了新的异常,从而造成程序崩溃的问题。

简述一下移动构造函数,什么库用到了这个函数?

C++11中新增了移动构造函数。与拷贝类似,移动也使用一个对象的值设置另一个对象的值。但是,又与拷贝不同的是,移动实现的是对象值真实的转移(源对象到目的对象):源对象将丢失其内容,其内容将被目的对象占有。移动操作的发生的时候,是当移动值的对象是未命名的对象的时候。这里未命名的对象就是那些临时变量,甚至都不会有名称。典型的未命名对象就是函数的返回值或者类型转换的对象。使用临时对象的值初始化另一个对象值,不会要求对对象的复制:因为临时对象不会有其它使用,因而,它的值可以被移动到目的对象。做到这些,就要使用移动构造函数和移动赋值:当使用一个临时变量对对象进行构造初始化的时候,调用移动构造函数。类似的,使用未命名的变量的值赋给一个对象时,调用移动赋值操作。

移动操作的概念对对象管理它们使用的存储空间很有用的,诸如对象使用new和delete分配内存的时候。在这类对象中,拷贝和移动是不同的操作:从A拷贝到B意味着,B分配了新内存,A的整个内容被拷贝到为B分配的新内存上。 而从A移动到B意味着分配给A的内存转移给了B,没有分配新的内存,它仅仅包含简单地拷贝指针。 看下面的例子:

// 移动构造函数和赋值
#include <iostream>
#include <string>
using namespace std;

class Example6 {
    string* ptr;
public:
    Example6 (const string& str) : ptr(new string(str)) {}
    ~Example6 () {delete ptr;}
    // 移动构造函数,参数x不能是const Pointer&& x,
    // 因为要改变x的成员数据的值;
    // C++98不支持,C++0x(C++11)支持
    Example6 (Example6&& x) : ptr(x.ptr) 
    {
        x.ptr = nullptr;
    }
    // move assignment
    Example6& operator= (Example6&& x) 
    {
        delete ptr; 
        ptr = x.ptr;
        x.ptr=nullptr;
        return *this;
    }
    // access content:
    const string& content() const {return *ptr;}
    // addition:
    Example6 operator+(const Example6& rhs) 
    {
        return Example6(content()+rhs.content());
    }
};
int main () {
    Example6 foo("Exam");           // 构造函数
    // Example6 bar = Example6("ple"); // 拷贝构造函数
    Example6 bar(move(foo));     // 移动构造函数
                                // 调用move之后,foo变为一个右值引用变量,
                                // 此时,foo所指向的字符串已经被"掏空",
                                // 所以此时不能再调用foo
    bar = bar+ bar;             // 移动赋值,在这儿"="号右边的加法操作,
                                // 产生一个临时值,即一个右值
                                 // 所以此时调用移动赋值语句
    cout << "foo's content: " << foo.content() << '\n';
    return 0;
}




执行结果:

foo's content: Example



请你回答一下 C++ 类内可以定义引用数据成员吗?

c++类内可以定义引用成员变量,但要遵循以下三个规则:

  1. 不能用默认构造函数初始化,必须提供构造函数来初始化引用成员变量。否则会造成引用未初始化错误。

  2. 构造函数的形参也必须是引用类型。

  3. 不能在构造函数里初始化,必须在初始化列表中进行初始化。

简述一下什么是常函数,有什么作用

类的成员函数后面加 const,表明这个函数不会对这个类对象的数据成员(准确地说是非静态数据成员)作任何改变。在设计类的时候,一个原则就是对于不改变数据成员的成员函数都要在后面加const,而对于改变数据成员的成员函数不能加 const。所以 const 关键字对成员函数的行为作了更明确的限定:有 const 修饰的成员函数(指 const 放在函数参数表的后面,而不是在函数前面或者参数表内),只能读取数据成员,不能改变数据成员;没有 const 修饰的成员函数,对数据成员则是可读可写的。除此之外,在类的成员函数后面加 const 还有什么好处呢?那就是常量(即 const)对象可以调用 const 成员函数,而不能调用非const修饰的函数。正如非const类型的数据可以给const类型的变量赋值一样,反之则不成立。

#include<iostream>
using namespace std;
 
class CStu
{
public:
    int a;
    CStu()
    {
        a = 12;
    }
 
    void Show() const
    {
        //a = 13; //常函数不能修改数据成员
        cout <<a << "I am show()" << endl;
    }
};
 
int main()
{
    CStu st;
    st.Show();
    system("pause");
    return 0;
}


请问拷贝构造函数的参数是什么传递方式,为什么

参考回答

  1. 拷贝构造函数的参数必须使用引用传递

  2. 如果拷贝构造函数中的参数不是一个引用,即形如CClass(const CClass c_class),那么就相当于采用了传值的方式(pass-by-value),而传值的方式会调用该类的拷贝构造函数,从而造成无穷递归地调用拷贝构造函数。因此拷贝构造函数的参数必须是一个引用。

    需要澄清的是,传指针其实也是传值,如果上面的拷贝构造函数写成CClass(const CClass* c_class),也是不行的。事实上,只有传引用不是传值外,其他所有的传递方式都是传值。

  1. class Aclass B1:public virtual A;class B2:public virtual A;class D:public B1,public B2;

  1. 虚继承的类可以被实例化,举例如下:

    class Animal {/* ... */ };class Tiger : virtual public Animal { /* ... */ };class Lion : virtual public Animal { /* ... */ }

    int main( ){

    Liger lg;

    /*既然我们已经在Tiger和Lion类的定义中声明了"virtual"关键字,于是下面的代码编译OK */int weight = lg.getWeight();

    }

简述一下拷贝赋值和移动赋值?

参考回答

  1. 拷贝赋值是通过拷贝构造函数来赋值,在创建对象时,使用同一类中之前创建的对象来初始化新创建的对象。

  2. 移动赋值是通过移动构造函数来赋值,二者的主要区别在于

    1)拷贝构造函数的形参是一个左值引用,而移动构造函数的形参是一个右值引用;

    2)拷贝构造函数完成的是整个对象或变量的拷贝,而移动构造函数是生成一个指针指向源对象或变量的地址接管源对象的内存,相对于大量数据的拷贝节省时间和内存空间。

类对象和类指针区别

  1. 定义:

    类对象:利用类的构造函数内存中分配一块区域(包括一些成员变量赋值);

    类指针:是一个内存地址值,指向内存中存放的类对象(类指针可以指向多个不同的对象,多态);

  2. 使用

    引用成员:对象使用“.”操作符,指针用“->”操作符;

  3. 存储位置

    类对象:用的是内存栈,是个局部的临时变量;

    类指针:用的是内存堆,是个永久变量,除非你释放它。 new/delete

  4. 指针可以实现多态,直接用对象不行。

五、模板

解释下 C++ 中类模板和模板类的区别

参考回答

  1. 类模板是模板的定义,不是一个实实在在的类,定义中用到通用类型参数

  2. 模板类是实实在在的定义,是类模板的实例化。类定义中参数被实际类型所代替。

答案解析

  1. 类模板的类型参数可以有一个或多个,每个类型前面都必须加class,如template <class T1,class T2>class someclass{…};在定义对象时分别代入实际的类型名,如 someclass<int,double> obj;

  2. 和使用类一样,使用类模板时要注意其作用域,只能在其有效作用域内用它定义对象。

  3. 模板可以有层次,一个类模板可以作为基类,派生出派生模板类。

说说模板类是在什么时候实现的

  1. 模板实例化:模板的实例化分为显示实例化和隐式实例化,前者是研发人员明确的告诉模板应该使用什么样的类型去生成具体的类或函数,后者是在编译的过程中由编译器来决定使用什么类型来实例化一个模板。不管是显示实例化或隐式实例化,最终生成的类或函数完全是按照模板的定义来实现的

  2. 模板具体化:当模板使用某种类型类型实例化后生成的类或函数不能满足需要时,可以考虑对模板进行具体化。具体化时可以修改原模板的定义,当使用该类型时,按照具体化后的定义实现,具体化相当于对某种类型进行特殊处理。

  3. 代码示例:

    #include <iostream>
    using namespace std;
    
    // #1 模板定义
    template<class T>
    struct TemplateStruct
    {
        TemplateStruct()
        {
            cout << sizeof(T) << endl;
        }
    };
    
    // #2 模板显示实例化
    template struct TemplateStruct<int>;
    
    // #3 模板具体化
    template<> struct TemplateStruct<double>
    {
        TemplateStruct() {
            cout << "--8--" << endl;
        }
    };
    
    int main()
    {
        TemplateStruct<int> intStruct;
        TemplateStruct<double> doubleStruct;
    
        // #4 模板隐式实例化
        TemplateStruct<char> llStruct;
    }
    
    
    
    

    运行结果:

    4
    --8--
    1

仿函数了解吗?有什么作用

参考回答

  1. 仿函数(functor)又称为函数对象(function object)是一个能行使函数功能的类。仿函数的语法几乎和我们普通的函数调用一样,不过作为仿函数的类,都必须重载operator()运算符,举个例子:

class Func{public:

void operator() (const string& str) const {
    cout<<str<<endl;
}


};

Func myFunc;myFunc("helloworld!");

helloworld!

  1. 仿函数既能像普通函数一样传入给定数量的参数,还能存储或者处理更多我们需要的有用信息。我们可以举个例子:

    假设有一个vector<string>,你的任务是统计长度小于5的string的个数,如果使用count_if函数的话,你的代码可能长成这样:

    bool LengthIsLessThanFive(const string& str) {

     return str.length()<5;    
    
    
    

    } int res=count_if(vec.begin(), vec.end(), LengthIsLessThanFive);

其中count_if函数的第三个参数是一个函数指针,返回一个bool类型的值。一般的,如果需要将特定的阈值长度也传入的话,我们可能将函数写成这样:

bool LenthIsLessThan(const string& str, int len) {
    return str.length()<len;
}


这个函数看起来比前面一个版本更具有一般性,但是他不能满足count_if函数的参数要求:count_if要求的是unary function(仅带有一个参数)作为它的最后一个参数。如果我们使用仿函数,是不是就豁然开朗了呢:

class ShorterThan {
public:
    explicit ShorterThan(int maxLength) : length(maxLength) {}
    bool operator() (const string& str) const {
        return str.length() < length;
    }
private:
    const int length;
};

猜你喜欢

转载自blog.csdn.net/shisniend/article/details/131908947