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

回答:

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

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

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

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

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

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

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

1.3.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 类中重写
}

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

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

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); // 输出 double 类型,注意 void * 转化为 double *
}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); // 调用 func_int 处理 int 类型c_func(func_double, &b); // 调用 func_double 处理 double 类型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()
{// 使用默认构造函数初始化对象 s1Student s1;// 使用初始化构造函数初始化对象 s2Student s2(1002, 18);return 0;
}

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

2. 拷贝构造函数

#include "stdafx.h"
#include "iostream.h"class Test
{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.只定义析构函数,会自动生成哪些构造函数

回答:

只定义了析构函数,编译器将自动为我们生成拷贝构造函数和默认构造函数。 默认构造函数和初始化构造函数。 在定义类的对象的时候,完成对象的初始化工作。

class Student
{
public:// 默认构造函数Student(){num = 1001;age = 18;}// 初始化构造函数Student(int n, int a) : num(n), age(a) {}private:int num;int age;
};int main()
{// 使用默认构造函数初始化对象 s1Student s1;// 使用初始化构造函数初始化对象 s2Student 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); // 使用带参构造函数初始化对象 t1Test t2(t1);   // 使用复制构造函数将 t1 复制给 t2return 0;
}

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

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

定义一个空类

class Empty{};

默认会生成以下几个函数

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

Empty(){}

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

Empty(const Empty& copy){}

3. 赋值运算符

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

4. 析构函数(非虚)

~Empty(){}

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

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){delete[] _str;  // 释放原有内存_str = new char[strlen(s._str) + 1];strcpy_s(_str, strlen(s._str) + 1, s._str);}return *this;
}

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

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

1.3.12.简述一下 C++ 中的多态

回答:

由于派生类重写基类方法,然后用基类引用指向派生类对象,调用方法时候会进行动态绑定,这就是多 态。 多态分为静态多态和动态多态:

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

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成员、private成员。类中可以直接访问自 己类的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,没有分配新的内存,它仅仅包含简单地拷贝指针。 看下面的例子:

#include <iostream>
#include <string>
using namespace std;class Example6 {string* ptr;
public:// 构造函数Example6(const string& str) : ptr(new string(str)) {}// 析构函数~Example6() { delete ptr; }// 移动构造函数Example6(Example6&& x) : ptr(x.ptr) {x.ptr = nullptr;}// 移动赋值运算符Example6& operator=(Example6&& x) {delete ptr;ptr = x.ptr;x.ptr = nullptr;return *this;}// 访问内容const string& content() const { return *ptr; }// 加法运算符重载Example6 operator+(const Example6& rhs) {return Example6(content() + rhs.content());}
};int main() {Example6 foo("Exam");          // 构造函数// 使用移动构造函数Example6 bar(move(foo));       // 调用 move 后,foo 变为一个右值引用变量// 移动赋值bar = bar + bar;               // 产生一个临时值,调用移动赋值运算符cout << "foo's content: " << foo.content() << '\n';return 0;
}

结果:foo's content: Example

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

回答:

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

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

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

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

1.3.17.构造函数为什么不能被声明为虚函数?

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

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

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

1.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; // 带const的函数不能修改数据成员cout << a << " I am show()" << endl;}
};int main()
{CStu st;st.Show();system("pause");return 0;
}

1.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; // 访问通过 B 继承的 _ad.C::_a = 2; // 访问通过 C 继承的 _ad._b = 3;d._c = 4;d._d = 5;cout << sizeof(D) << endl;return 0;
}

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

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

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

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

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

1. 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:xiaosireturn 0;
}

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

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

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

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

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

#include <iostream>
using namespace std;class Base {
public:virtual void func() = 0; // 纯虚函数
};class Derived : public Base {
public:void func() override { // 重写纯虚函数cout << "哈哈" << endl;}
};int main() {Base *b = new Derived(); // 基类指针指向派生类对象b->func();              // 调用派生类的实现return 0;
}

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

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

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

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

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

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

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

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

4. 虚函数和纯虚函数通常存在于抽象基类之中,被继承的子类重载,目的是提供一个统一的接口。 5. 虚函数的定义形式: virtual{} ;纯虚函数的定义形式: virtual { } = 0;在虚函数和纯虚函 C 数的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时要求前期绑定,然而虚 函数却是动态绑定,而且被两者修饰的函数生命周期也不一样。

1.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。

现在,问题是如果我们有这种继承结构会出现什么样的问题。 看看下面的代码后再来回答问题吧。

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方法是不明确的,因此不能通过编译。

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

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

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

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

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

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

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

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

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

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

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

1.3.27.如何理解抽象类?

1. 抽象类的定义如下:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

#include <iostream>
using namespace std;class Func {
public:void operator()(const string& str) const {cout << str << endl;}
};int main() {Func myFunc;myFunc("helloworld!");return 0;
}

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

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

#include <iostream>
#include <vector>
#include <algorithm> // for count_ifbool LengthIsLessThanFive(const std::string& str) {return str.length() < 5;
}int main() {std::vector<std::string> vec = {"apple", "banana", "cat", "dog", "elephant"};int res = std::count_if(vec.begin(), vec.end(), LengthIsLessThanFive);std::cout << "Number of strings with length less than 5: " << res << std::endl;return 0;
}

其中c ount_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;
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/913378.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/913378.shtml
英文地址,请注明出处:http://en.pswp.cn/news/913378.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构之二叉平衡树

系列文章目录 数据结构之ArrayList_arraylist o(1) o(n)-CSDN博客 数据结构之LinkedList-CSDN博客 数据结构之栈_栈有什么方法-CSDN博客 数据结构之队列-CSDN博客 数据结构之二叉树-CSDN博客 数据结构之优先级队列-CSDN博客 常见的排序方法-CSDN博客 数据结构之Map和Se…

Maven引入第三方JAR包实战指南

要将第三方提供的 JAR 包引入本地 Maven 仓库&#xff0c;可通过以下步骤实现&#xff08;以 Oracle JDBC 驱动为例&#xff09;&#xff1a;&#x1f527; 方法 1&#xff1a;使用 install:install-file 命令&#xff08;推荐&#xff09;定位 JAR 文件 将第三方 JAR 包&#…

JavaSE -- 泛型详细介绍

泛型 简介 集合存储数据底层是利用 Object 来接收的&#xff0c;意思是说如果不对类型加以限制&#xff0c;所有数据类型柔和在一起&#xff0c;这时如何保证数据的安全性呢&#xff08;如果不限制存入的数据类型&#xff0c;任何数据都能存入&#xff0c;当我们取出数据进行强…

使用 Python 实现 ETL 流程:从文本文件提取到数据处理的全面指南

文章大纲&#xff1a; 引言&#xff1a;什么是 ETL 以及其重要性 ETL&#xff08;提取-转换-加载&#xff09;是数据处理领域中的核心概念&#xff0c;代表了从源数据到目标系统的三个关键步骤&#xff1a;**提取&#xff08;Extract&#xff09;**数据、**转换&#xff08;Tra…

selenium基础知识 和 模拟登录selenium版本

前言 selenium框架是Python用于控制浏览器的技术,在Python爬虫获取页面源代码的时候,是最重要的技术之一,通过控制浏览器,更加灵活便捷的获取浏览器中网页的源代码。 还没有安装启动selenium的同志请先看我的上一篇文章进行配置启动 和 XPath基础 对selenium进行浏览器和驱动…

JS 网页全自动翻译v3.17发布,全面接入 GiteeAI 大模型翻译及自动部署

两行 js 实现 html 全自动翻译。 无需改动页面、无语言配置文件、无 API Key、对 SEO 友好&#xff01; 升级说明 translate.service 深度绑定 GiteeAI 作为公有云翻译大模型算力支持translate.service 增加shell一键部署后通过访问自助完成GiteeAI的开通及整个接入流程。增加…

数据结构:数组:插入操作(Insert)与删除操作(Delete)

目录 插入操作&#xff08;Inserting in an Array&#xff09; 在纸上模拟你会怎么做&#xff1f; 代码实现 复杂度分析 删除操作&#xff08;Deleting from an Array&#xff09; 在纸上模拟一下怎么做&#xff1f; 代码实现 复杂度分析 插入操作&#xff08;Inserti…

Qt之修改纯色图片的颜色

这里以修改QMenu图标颜色为例,效果如下: MyMenu.h #ifndef MYMENU_H #define MYMENU_H#include <QMenu>class MyMenu : public QMenu { public:explicit MyMenu(QWidget *parent = nullptr);protected:void mouseMoveEvent(QMouseEvent *event) override; };#endif /…

uni-app实现单选,多选也能搜索,勾选,选择,回显

前往插件市场安装插件下拉搜索选择框 - DCloud 插件市场&#xff0c;该插件示例代码有vue2和vue3代码 是支持微信小程序和app的 示例代码&#xff1a; <template><view><!-- 基础用法 --><cuihai-select-search:options"options"v-model&quo…

【机器学习深度学习】 微调的十种形式全解析

目录 一、为什么要微调&#xff1f; 二、微调的 10 种主流方式 ✅ 1. 全参数微调&#xff08;Full Fine-tuning&#xff09; ✅ 2. 冻结部分层微调&#xff08;Partial Fine-tuning&#xff09; ✅ 3. 参数高效微调&#xff08;PEFT&#xff09; &#x1f538; 3.1 LoRA&…

信刻光盘安全隔离与文件单向导入/导出系统

北京英特信网络科技有限公司成立于2005年&#xff0c;是专业的数据光盘摆渡、刻录分发及光盘存储备份领域的科技企业&#xff0c;专注为军队、军工、司法、保密等行业提供数据光盘安全摆渡、跨网交换、档案归档检测等专业解决方案。 公司立足信创产业&#xff0c;产品国产安全可…

Python-标准库-os

1 需求 2 接口 3 示例 4 参考资料 在 Python 中&#xff0c;os&#xff08;Operating System&#xff09;模块是一个非常重要的内置标准库&#xff0c;提供了许多与操作系统进行交互的函数和方法&#xff0c;允许开发者在 Python 程序中执行常见的操作系统任务&#xff0c;像文…

OpenCV CUDA模块设备层-----在 GPU 上执行类似于 std::copy 的操作函数warpCopy()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 OpenCV 的 CUDA 模块&#xff08;cudev&#xff09; 中的一个设备端内联模板函数&#xff0c;用于在 GPU 上执行类似于 std::copy 的操作&#xff…

Vue Router 中$route.path与 params 的关系

1. params 参数的本质&#xff1a;路径的动态片段在 Vue Router 中&#xff0c;params 参数是通过路由配置的动态路径片段定义的&#xff0c;例如&#xff1a;// 路由配置{ path: /user/:id, component: User }当访问/user/123时&#xff0c;/user/123是完整的路径&#xff0c;…

React 极简响应式滑块验证组件实现,随机滑块位置

&#x1f3af; 滑块验证组件 (Slider Captcha) 一个现代化、响应式的滑块验证组件&#xff0c;专为 React 应用设计&#xff0c;提供流畅的用户体验和强大的安全验证功能。 ✨ 功能特性 &#x1f3ae; 核心功能 智能滑块拖拽 – 支持鼠标和触摸屏操作&#xff0c;响应灵敏随…

STM32第十六天蓝牙模块

一&#xff1a;蓝牙模块HC-05 1&#xff1a;硬件引脚配置&#xff1a; | 标号 | PIN | 说明 | |------|-------|---------------------------------------| | 1 | START | 状态引出引脚&#xff08;未连接/连接输出信号时&#xff09; |…

时序数据库IoTDB用户自定义函数(UDF)使用指南

1. 编写UDF时序数据库IoTDB为用户提供了编写UDF的JAVA API&#xff0c;用户可以自主实现UDTF&#xff08;用户自定义转换函数&#xff09;类&#xff0c;IoTDB将通过类加载机制装载用户编写的类。Maven依赖如果使用Maven&#xff0c;可以从Maven库中搜索以下依赖&#xff0c;并…

Linux国产与国外进度对垒

Linux国产与国外进度对垒 引言国产Linux的发展现状国外Linux的发展现状技术对比国产Linux的挑战与机遇国外Linux的优势与局限结论 引言 简述Linux在全球操作系统市场中的地位国产Linux的发展背景与意义国外主流Linux发行版的现状 国产Linux的发展现状 主要国产Linux发行版介…

Jenkins-Email Extension 插件插件

Editable Email Notification Editable Email Notification 是 Jenkins 的 Email Extension 插件的核心功能&#xff0c;用于自定义邮件通知&#xff0c;包括邮件主题、内容、收件人、发件人等 属性 1.Project From 项目发件人&#xff0c;设置邮件的发件人地址 **注意&…

windows系统下将Docker Desktop安装到除了C盘的其它盘中

windows系统下安装docker会自动安装到C盘&#xff0c;可以采用下面的方法将其安装到其它盘中1、先下载Docker Desktop安装程序Docker Desktop Installer.exe&#xff0c;比如你下载到了C:\Users\YourUsername\Downloads 文件夹中。 2、打开 PowerShell 进入C:\Users\YourUser…