C++:3.面向对象

1.3.1 简述一下什么是面向对象、

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

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

面向过程:根据业务逻辑从上到下写代码

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

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

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

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

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

3. 多态:用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。实现 多态,有二种方式,重写,重载。

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

1. 重写

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

2. 重载

我们在平时写代码中会用到几个函数但是他们的实现功能相同,但是有些细节却不同。例如:交换 两个数的值其中包括(int, float,char,double)这些个类型。在C语言中我们是利用不同的函数名来 加以区分。这样的代码不美观而且给程序猿也带来了很多的不便。于是在C++中人们提出了用一个 函数名定义多个函数,也就是所谓的函数重载。函数重载是指同一可访问区内被声明的几个具有不 同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不 关心函数返回类型。

1.3.4 说说 C++ 的重载和重写是如何实现的

1. 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,加上参数首字母以区分同名函数。

2. 在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调 用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类 的函数。

  • 1. 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。
  • 2. 存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始的虚指 针。虚表是和类对应的,虚表指针是和对象对应的。
  • 3. 多态性是一个接口多种实现,是面向对象的核心,分为类的多态性和函数的多态性。
  • 4. 重写用虚函数来实现,结合动态绑定。
  • 5. 纯虚函数是虚函数再加上 = 0。
  • 6. 抽象类是指包括至少一个纯虚函数的类。

纯虚函数:virtual void fun()=0。即抽象类必须在子类实现这个函数,即先有名称,没有内容,在 派生类实现内容。

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

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

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.3.7 只定义析构函数,会自动生成哪些构造函数

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

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

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

定义一个空类默认会生成以下几个函数

  • 1. 无参的构造函数 在定义类的对象的时候,完成对象的初始化工作。
  • 2. 拷贝构造函数 拷贝构造函数用于复制本类的对象
  • 3. 赋值运算符
  • 4. 析构函数(非虚)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1.3.12 简述一下 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. 通过基类类型的指针或引用来调用虚函数。

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

1.3.13 说说为什么要虚析构,为什么不能虚构造

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

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

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

2. 不能虚构造:

  • 1. 从存储空间角度:虚函数对应一个vtale,这个表的地址是存储在对象的内存空间的。如果将构 造函数设置为虚函数,就需要到vtable 中调用,可是对象还没有实例化,没有内存空间分 配,如何调用。(悖论)
  • 2. 从使用角度:虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造 函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚 函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个 成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调 用,因此也就规定构造函数不能是虚函数。
  • 3. 从实现上看,vbtl 在构造函数调用后才建立,因而构造函数不可能成为虚函数。从实际含义 上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而 且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有 太大的必要成为虚函数。

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

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

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

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

类中的成员可以分为三种类型,分别为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成员。

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

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

移动操作的概念对对象管理它们使用的存储空间很有用的,诸如对象使用new和delete分配内存的时 候。在这类对象中,拷贝和移动是不同的操作:从A拷贝到B意味着,B分配了新内存,A的整个内容被 拷贝到为B分配的新内存上。

而从A移动到B意味着分配给A的内存转移给了B,没有分配新的内存,它仅仅包含简单地拷贝指针。 看下面的例子:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

class Func{
public:
    void operator() (const string& str) const {
        cout<<str<<endl;
    }
};
Func myFunc;
myFunc("helloworld!");
>>>helloworld

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

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

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

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

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

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

这个原因很简单,主要是从语义上考虑,所以不支持。因为构造函数本来就是为了明确初始化对象 成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。 另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函 数来完成你想完成的动作。(这不就是典型的悖论)

构造函数用来创建一个新的对象,而虚函数的运行是建立在对象的基础上,在构造函数执行时,对象尚 未形成,所以不能将构造函数定义为虚函数

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

其实很简单,那内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在 继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline函数在编译时被展 开,虚函数在运行时才能动态的绑定函数) 内联函数是在编译时期展开,而虚函数的特性是运行时才动态联编,所以两者矛盾,不能定义内联函数 为虚函数

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

这也很简单,静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他也没 有要动态绑定的必要性。 静态成员函数属于一个类而非某一对象,没有this指针,它无法进行对象的判别

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

因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。

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

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

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

1.3.36 虚函数表里存放的内容是什么时候写进去的?

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

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

  • 11
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值