基础知识学习---牛客网C++面试宝典(三)C/C++基础之面向对象

1、本栏用来记录社招找工作过程中的内容,包括基础知识学习以及面试问题的记录等,以便于后续个人回顾学习; 暂时只有2023年3月份,第一次社招找工作的过程;

2、个人经历: 研究生期间课题是SLAM在无人机上的应用,有接触SLAM、Linux、ROS、C/C++、DJI OSDK等;
3、参加工作后(2021-2023年)岗位是嵌入式软件开发,主要是服务器开发,Linux、C/C++、网络编程、docker容器、CMake、makefile、Shell脚本、JSON等。

4、求职岗位是嵌入式软件开发、C/C++开发、自动驾驶岗位开发等。

在这里插入图片描述

此系列为在学习牛客网C++面试宝典过程中记录的笔记,本篇记录第一章C/C++基础部分的第三节:面向对象。

牛客网C++面试宝典链接:https://www.nowcoder.com/issue/tutorial?tutorialId=93&uuid=2a565e401f31468cabb3a378c60d5049

文章目录

3.1 简述一下什么是面向对象

参考回答

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

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

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

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

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

参考回答

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

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

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

三种继承方式

在这里插入图片描述

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

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

参考回答

重写

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

示例如下:

#include<iostream>  

using namespace std;

class A 
{
 public:  
	 virtual void fun() 
	 {  
	 	 cout << "A" << endl; 
	 }
}; 
class B :public A 
{ 
public:  
	virtual void fun() 
	{  
		 cout << "B" << endl; 
	}
}; 

int main(void) 
{ 
	A* a = new B(); 
	a->fun();//输出B,A类中的fun在B类中重写 

	return 0;
}

结果:
在这里插入图片描述

重载

我们在平时写代码中会用到几个函数但是他们的实现功能相同,但是有些细节却不同。例如:交换两个数的值其中包括(int, float,char,double)这些个类型。在C语言中我们是利用不同的函数名来加以区分。这样的代码不美观而且给程序猿也带来了很多的不便。于是在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){}; 
};

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

参考答案

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

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

在这里插入图片描述
由上图可得,d代表double,f代表float,i代表int,加上参数首字母以区分同名函数。

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

用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。

存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。

多态性是一个接口多种实现,是面向对象的核心,分为类的多态性函数的多态性

重写用虚函数来实现,结合动态绑定。

纯虚函数是虚函数再加上 = 0。

抽象类是指包括至少一个纯虚函数的类。

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

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

参考答案

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

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

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

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

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、移动构造函数。用于将其他类型的变量,隐式转换为本类对象。

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

参考答案

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

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

答案解析

示例如下:

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了两次,这样产生一个未定义的错误,所以说,如果一个类定义了析构函数,那么它要定义自己的拷贝构造函数和默认构造函数。

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

参考答案

定义一个空类

class Empty
{
};

默认会生成以下几个函数
1、无参的构造函数

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

Empty()
{
}

2、拷贝构造函数

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

Empty(const Empty& copy)
{
}

3、赋值运算符

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

4、析构函数(非虚)

~Empty()
{
}

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

参考答案

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3.12 简述一下 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;
}

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

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

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

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

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

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

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

2、不能虚构造:

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

  • 从使用角度:虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。

  • 从实现上看,vbtl 在构造函数调用后才建立,因而构造函数不可能成为虚函数。从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数。

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

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

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成员。

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

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

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

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

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

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

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

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

类的成员函数后面加 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;
}

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

虚继承是解决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实际上是虚基表指针,指向虚基表。

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

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

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)否则该派生类也不能实例化,而且它们在抽象类中往往没有定义。定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

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

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

参考回答

1、纯虚函数不可以实例化,但是可以用其派生类实例化,示例如下:

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

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

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

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

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

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

参考回答

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

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

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

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

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

3.24 说说 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。

现在,问题是如果我们有这种继承结构会出现什么样的问题。

看看下面的代码后再来回答问题吧。

/*  *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 
{ 
	/* ... */ 
};

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

编译时报错如下:
在这里插入图片描述

在我们的继承结构中,我们可以看出Tiger和Lion类都继承自Animal基类。所以问题是:因为Liger多重继承了Tiger和Lion类,因此Liger类会有两份Animal类的成员(数据和方法),Liger对象"lg"会包含Animal基类的两个子对象。

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

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

/*  *Animal类对应于图表的类A* */ 
class Animal 
{ 
	/* ... */    
	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;   /*编译错误,下面的代码不会被任何C++编译器通过 */   
	int weight = lg.getWeight();   
	return 0; 
}

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

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

参考回答

不要在构造函数中调用虚方法,从语法上讲,调用完全没有问题,但是从效果上看,往往不能达到需要的目的。

派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。

同样,进入基类析构函数时,对象也是基类类型。

所以,虚函数始终仅仅调用基类的虚函数(如果是基类调用虚函数),不能达到多态的效果,所以放在构造函数中是没有意义的,而且往往不能达到本来想要的效果。

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

参考回答

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

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

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

3.27 说说类方法和数据的权限有哪几种

参考回答

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

3.28 如何理解抽象类?

参考回答

抽象类的定义如下:

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

抽象类有如下几个特点:

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

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

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

3.29 什么是多态?除了虚函数,还有什么方式能实现多态?

参考回答

关于多态,简而言之就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”。

多态是面向对象的重要特性之一,它是一种行为的封装,就是不同对象对同一行为会有不同的状态。(举例 : 学生和成人都去买票时,学生会打折,成人不会)

多态是以封装和继承为基础的。在C++中多态分为静态多态(早绑定)和动态多态(晚绑定)两种,其中动态多态是通过虚函数实现,静态多态通过函数重载实现,代码如下:

class A
{
public:    
    void do(int a);    
    void do(int a, int b);
};

3.30 简述一下虚析构函数,什么作用

参考回答

虚析构函数,是将基类的析构函数声明为virtual,举例如下:

class TimeKeeper
{
public:    
    TimeKeeper() {}        
    virtual ~TimeKeeper() {}    
};

虚析构函数的主要作用是防止内存泄露

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

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

3.31 说说什么是虚基类,可否被实例化?

参考回答

被继承的类前面加上virtual关键字,这时被继承的类称为虚基类,代码如下:

class A
class B1:public virtual A;
class B2:public virtual A;
class D:public B1,public B2;

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

class Animal {/* ... */ };
class Tiger : virtual public Animal { /* ... */ };
class Lion : virtual public Animal { /* ... */ }
int main( )
{
    Liger lg;

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

请参考上面的3.24 说说 C++ 中什么是菱形继承问题,如何解决

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

参考回答

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

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

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

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

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、仿函数既能想普通函数一样传入给定数量的参数,还能存储或者处理更多我们需要的有用信息。我们可以举个例子:

假设有一个vector,你的任务是统计长度小于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;
 };

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

参考回答

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

参考回答

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

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

答案解析

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

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

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

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

参考回答

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

乘凉~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值