章节大纲:
1、面向对象八股文(长期更新_整理收集_排版已优化_day01_20个)
2、面向对象八股文(长期更新_整理收集_排版未优化_day02_20个)
3、面向对象八股文(长期更新_整理收集_排版未优化_day03_20个)
4、面向对象八股文(长期更新_整理收集_排版未优化_day04_20个)
目录大纲
1、 简述一下什么是面向对象
2、简述一下面向对象的三大特征
3、简述一下 C++ 的重载和重写,以及它们的区别
4、命名倾轧(name mangling)技术
5、 说说构造函数有几种,分别什么作用
6、 只定义析构函数,会自动生成哪些构造函数
7、 说说一个类,默认会生成哪些函数
8、 说说 C++ 类对象的初始化顺序,有多重继承情况下的顺序
9、 简述下向上转型和向下转型
10、 简述下深拷贝和浅拷贝,如何实现深拷贝
11、 简述一下 C++ 中的多态
12、 说说为什么要虚析构,为什么不能虚构造
13、 说说模板类是在什么时候实现的
14 、说说类继承时,派生类对不同关键字修饰的基类方法的访问权限
15 、简述一下移动构造函数,什么库用到了这个函数?
16 、构造函数为什么不能被声明为虚函数?
17 、简述一下什么是常函数,有什么作用
18、 说说什么是虚继承,解决什么问题,如何实现?
19、 简述一下虚函数和纯虚函数,以及实现原理
20、 说说纯虚函数能实例化吗,为什么?派生类要实现吗,为什么?
1、简述一下什么是面向对象
(1)面向对象是一种编程思想,把一切东西看成是一个个对象,把这些对象拥有的属性变量和操作这些属性变量的函数打包成一个类来表示
(2)面向过程和面向对象的区别:
①面向过程: 根据业务逻辑从上到下写代码
②面向对象: 将数据与函数绑定到一起,进行封装,加快开发程序,减少重复代码的重写过程
2、简述一下面向对象的三大特征
面向对象的三大特征是封装、继承、多态。
1、封装: 封装是将对象的状态(成员变量)
和行为(成员函数)
封装在一起,通过访问修饰符对外部隐藏对象的内部实现细节。封装通过提供公共接口来控制对对象的访问,使得对象的状态只能通过定义的方法进行操作,从而实现了信息隐藏、提高了安全性,并降低了系统的复杂性。
2、继承: 继承是一种机制,允许一个类(派生类)
基于另一个类(基类)
的定义来创建。通过继承,派生类可以继承基类的属性和方法,同时可以添加新的属性和方法或者修改已有的方法。继承提供了代码重用的机制,通过构建类的层次结构,可以更容易地扩展和维护代码。
三种继承方式
继承方式 | private 继承 | protected继承 | pubiic继承 |
---|---|---|---|
基类的的private成员 | 不可见 | 不可见 | 不可见 |
基类的的protected成员 | 变为private成员 | 仍为protected成员 | 仍为protected成员 |
基类的的public成员 | 变为private成员 | 变为protect成员 | 仍为public成员 |
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、重载
重载
是指在一个类中定义多个同名的函数,它们具有不同的参数列表(包括参数的个数或类型不同)。重载的目的是为了实现相同的操作但使用不同的参数列表。重载函数可以是普通的成员函数或虚函数,但它们不能同时是const或volatile的。重载函数的返回类型可以相同,也可以不同。
#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){};
};
4 命名倾轧(name mangling)技术
C++利用命名倾轧(name mangling)
技术主要是为了解决函数重载的问题。在C++中,函数重载允许我们定义多个同名的函数,只要它们的参数列表(参数类型和数量)不同就可以。然而,这给编译器如何区分这些函数带来了挑战。
命名倾轧
是一种编译器用来在编译期间为重载函数生成唯一标识符的技术。当编译器遇到一个重载函数时,它会为每个重载版本的函数生成一个唯一的名称,这些名称是根据函数的参数类型和数量进行生成的。这样,即使所有重载函数的名字相同,编译器也能够通过检查函数的唯一标识符(也就是名称倾轧后的名称)来区分它们。
例如,如果我们有以下两个重载函数:
void foo(int);
void foo(double);
编译器可能会将第一个函数命名为_Z3fooi
,将第二个函数命名为_Z3food
。这里_Z3
是函数签名的开始,foo
是函数名,后面的字符表示函数的参数类型。
这种技术允许编译器在编译期间生成唯一的符号表,使得链接器可以正确地链接所有的函数调用,即使这些函数在源代码中是重载的。
(1)用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。
(2)存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。
(3) 多态性是一个接口多种实现,是面向对象的核心,分为类的多态性和函数的多态性。
(4)重写用虚函数来实现,结合动态绑定。
(5)纯虚函数是虚函数再加上 = 0。 抽象类是指包括至少一个纯虚函数的类
5、 说说构造函数有几种,分别什么作用
在C++中,构造函数是一种特殊类型的成员函数,用于在对象被创建时初始化对象的各个成员变量。构造函数的名字与类名相同,没有返回类型,并且可以有多种形式。以下是一些常见的构造函数类型及其作用
构造函数有四种类型,包括默认构造函数
、带参数的构造函数
、拷贝构造函数
、移动构造函数
。
1. 默认构造函数(Default Constructor):
默认构造
函数是一种不带任何参数
的构造函数,如果在类中没有显式声明构造函数,并且没有使用其他构造函数形成对象,编译器将提供一个默认构造函数。默认构造函数通常用于执行基本的对象初始化工作
class MyClass {
public:
// 默认构造函数
MyClass() {
// 初始化工作
}
};
2. 带参数的构造函数(Parameterized Constructor):
带参数的构造函数接受一定数量的参数,用于在创建对象时提供初始值
class Point {
public:
// 带参数的构造函数
Point(int x, int y) {
this->x = x;
this->y = y;
}
private:
int x, y;
};
3. 拷贝构造函数(Copy Constructor):
拷贝构造函数用于通过使用同类型的其他对象来初始化对象。它通常在对象赋值、传递参数时被调用。
class MyClass {
public:
// 拷贝构造函数
MyClass(const MyClass& other) {
// 进行拷贝操作
}
};
4. 移动构造函数(Move Constructor):
C++11引入了移动构造函数,用于在对象之间进行资源的转移,提高性能。移动构造函数通过使用右值引用(&&)
来实现。
class MyString {
public:
// 移动构造函数
MyString(MyString&& other) noexcept {
// 进行资源的移动操作
}
};
这些构造函数类型可以根据需要在一个类中进行组合使用,以确保对象在创建时以适当的方式进行初始化。构造函数的正确使用对于确保对象的正确初始化和行为非常重要。
6、 只定义析构函数,会自动生成哪些构造函数
当你只定义了析构函数而没有定义其他构造函数时,C++编译器会自动生成默认构造函数
、拷贝构造函数
、移动构造函数
、拷贝赋值运算符
和移动赋值运算符
。这被称为"Big Five"或"Rule of Five"。这是因为这五个特殊成员函数在处理资源管理和对象拷贝/赋值方面非常重要。
以下是这五个特殊成员函数的默认生成规则:
1、默认构造函数(Default Constructor): 如果你没有定义任何构造函数,编译器将生成一个默认构造函数。这个构造函数对对象进行基本的默认初始化。
2、拷贝构造函数(Copy Constructor): 如果你没有定义自己的拷贝构造函数,编译器会生成一个拷贝构造函数,用于执行对象的浅拷贝。
3、移动构造函数(Move Constructor): 如果你没有定义自己的移动构造函数,编译器会生成一个移动构造函数,用于执行对象的移动操作。
4、拷贝赋值运算符(Copy Assignment Operator):如果你没有定义自己的拷贝赋值运算符,编译器会生成一个默认的拷贝赋值运算符,用于执行对象的浅拷贝。
5、移动赋值运算符(Move Assignment Operator): 如果你没有定义自己的移动赋值运算符,编译器会生成一个默认的移动赋值运算符,用于执行对象的移动操作。
这些默认生成的函数可能不足以处理复杂的资源管理或特殊的对象拷贝/赋值需求。在这种情况下,你可能需要显式地定义这些函数,并根据你的需求进行自定义实现。
7、 说说一个类,默认会生成哪些函数
当你创建一个类时,如果你没有显式定义一些特殊成员函数(构造函数、析构函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符),C++编译器会自动生成它们。这被称为"Big Five"或"Rule of Five",因为这包括五个与资源管理和对象生命周期有关的特殊成员函数。在现代C++中,也可以包括默认构造函数和析构函数。
以下是默认情况下会被生成的函数:
1、默认构造函数(Default Constructor): 如果你没有定义任何构造函数,编译器会生成一个默认构造函数。这个构造函数对对象进行基本的默认初始化。
2、析构函数(Destructor): 如果你没有定义自己的析构函数,编译器会生成一个默认的析构函数。这个析构函数用于释放对象所占用的资源。
3、拷贝构造函数(Copy Constructor): 如果你没有定义自己的拷贝构造函数,编译器会生成一个默认的拷贝构造函数。这个函数用于执行对象的浅拷贝。
4、拷贝赋值运算符(Copy Assignment Operator): 如果你没有定义自己的拷贝赋值运算符,编译器会生成一个默认的拷贝赋值运算符。这个函数用于执行对象的浅拷贝。
5、移动构造函数(Move Constructor): 如果你没有定义自己的移动构造函数,编译器会生成一个默认的移动构造函数。这个函数用于执行对象的移动操作。
6、移动赋值运算符(Move Assignment Operator): 如果你没有定义自己的移动赋值运算符,编译器会生成一个默认的移动赋值运算符。这个函数用于执行对象的移动操作。
这些函数是编译器为你的类生成的默认实现。在大多数情况下,它们可能是适当的。但如果你的类需要特殊的资源管理,你可能需要显式地定义其中一些或全部。
8、 说说 C++ 类对象的初始化顺序,有多重继承情况下的顺序
在C++中,类对象的初始化顺序取决于多个因素,包括类的继承关系和成员变量的声明顺序。下面我将简要说明类对象的初始化顺序:
1、单继承情况下的初始化顺序:
对于单一继承,初始化顺序是从基类到派生类。首先调用基类的构造函数,然后按照它们在继承链中的顺序逐级调用派生类的构造函数。
注释:成员变量初始化:在派生类构造函数体执行之前,派生类中的成员变量按照在类中的声明顺序进行
初始化。在 C++ 中,构造函数的初始化列表是在构造函数体执行之前执行的,它用于初始化成员变量。
这个初始化列表是在构造函数的定义中,冒号 : 之后,构造函数体之前的位置。
2、多继承情况下的初始化顺序:
在多继承的情况下,类对象的初始化顺序取决于继承声明的顺序。C++中多继承的形式可以是虚拟继承(virtual inheritance)或非虚拟继承。对于多重继承,初始化顺序是按照继承列表的顺序来的。在继承列表中,最左边的基类首先被初始化,然后依次向右初始化。
注释
1、虚拟继承: 如果基类使用了虚拟继承,派生类在构造时会调用虚基类的构造函数。在这种情况下,派生类中的虚基类构造函数的调用顺序是根据其在类继承列表中的声明顺序,从左到右,自上而下。
2、非虚拟继承: 如果基类使用了非虚拟继承,派生类在构造时会直接调用每个基类的构造函数,而不考虑继承列表中的声明顺序。构造函数的调用顺序取决于派生类中基类的声明顺序。
在多继承的情况下,务必注意构造函数的调用顺序,以确保对象正确地初始化。如果存在虚拟继承,要注意虚基类的构造函数只会被调用一次,而且是在最终派生类中。这些规则对于析构函数同样适用,析构函数的调用顺序是与构造函数相反的。
9、 简述下向上转型和向下转型
在面向对象编程中,向上转型(upcasting)和向下转型(downcasting)是两个常见的类型转换操作,通常用于处理继承关系中的类对象。
1)向上转型(Upcasting)
向上转型是将派生类对象的指针或引用转换为基类类型的指针或引用。这种转换是隐式的,不需要显式的类型转换操作符。
特点:
- 向上转型是安全的,不会丢失对象的任何数据。
- 转换后的对象只能访问基类中定义的成员,而不能访问派生类中新定义的成员。
示例:
class Base {
public:
void baseMethod() {
std::cout << "Base method" << std::endl;
}
};
class Derived : public Base {
public:
void derivedMethod() {
std::cout << "Derived method" << std::endl;
}
};
int main() {
Derived derivedObj;
Base* basePtr = &derivedObj; // 向上转型,Derived* 转换为 Base*
basePtr->baseMethod(); // 访问基类方法
// basePtr->derivedMethod(); // 错误:不能访问派生类方法
return 0;
}
2向下转型(Downcasting)
向下转型是将基类对象的指针或引用转换为派生类类型的指针或引用。这种转换需要显式的类型转换操作符,例如 dynamic_cast
。向下转型存在风险,可能会失败,因此通常需要进行类型检查。
特点:
- 向下转型可能会失败,因此通常需要进行类型检查。
- 转换后的对象可以访问派生类中的成员。
示例:
class Base {
public:
virtual void baseMethod() {
std::cout << "Base method" << std::endl;
}
};
class Derived : public Base {
public:
void derivedMethod() {
std::cout << "Derived method" << std::endl;
}
};
int main() {
Base* basePtr = new Derived(); // 向上转型
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 向下转型
if (derivedPtr) {
derivedPtr->derivedMethod(); // 访问派生类方法
} else {
std::cout << "Invalid cast" << std::endl;
}
delete basePtr;
return 0;
}
在上面的示例中,dynamic_cast
用于将基类指针 basePtr
转换为派生类指针 derivedPtr
。如果转换成功,derivedPtr
不为 nullptr
,可以安全地调用派生类方法;否则,转换失败,输出 “Invalid cast”。
总结
-
向上转型(Upcasting):
- 将派生类对象转换为基类对象。
- 安全、隐式转换。
- 只能访问基类成员。
-
向下转型(Downcasting):
- 将基类对象转换为派生类对象。
- 需要显式转换,使用
dynamic_cast
进行类型检查。 - 可以访问派生类成员,但存在失败的风险。
了解和正确使用向上转型和向下转型,可以更好地处理对象的多态性和继承关系。
10、 简述下深拷贝和浅拷贝,如何实现深拷贝
深拷贝和浅拷贝是与对象拷贝相关的两个概念,涉及到对象中包含的动态分配内存的处理方式
1、浅拷贝(Shallow Copy):
浅拷贝是指简单地复制对象的成员变量值,包括指针。如果对象包含指向堆内存的指针,浅拷贝只是复制指针的值,而不复制指针所指向的内存。这意味着两个对象共享相同的内存,当其中一个对象释放这块内存时,另一个对象可能访问到无效的内存。
class ShallowCopy {
public:
int* data;
ShallowCopy(const ShallowCopy& other) {
// 浅拷贝,只复制指针值
data = other.data;
}
};
2、深拷贝(Deep Copy):
深拷贝是指复制对象及其所有动态分配的资源,而不仅仅是复制指针。在深拷贝中,对象拷贝后,新对象和原对象的指针指向不同的内存块,每个对象都有独立的一份拷贝
class DeepCopy {
public:
int* data;
DeepCopy(const DeepCopy& other) {
// 深拷贝,分配新内存并复制数据
data = new int(*other.data);
}
~DeepCopy() {
delete data; // 在析构函数中释放动态分配的内存
}
};
3、实现深拷贝:
在实现深拷贝时,关键是要为新对象分配新的内存,并将原对象的数据复制到新内存中。如果对象包含多个指针成员或嵌套的对象,需要递归地进行深拷贝。
class ComplexObject {
public:
int* array;
int size;
ComplexObject(const ComplexObject& other) {
// 深拷贝,分配新内存并复制数据
size = other.size;
array = new int[size];
for (int i = 0; i < size; ++i) {
array[i] = other.array[i];
}
}
~ComplexObject() {
delete[] array;
}
};
在上述例子中,ComplexObject 包含了一个动态分配的整数数组,深拷贝的构造函数确保了数组的复制。需要注意,在析构函数中也要释放相应的内存,以防止内存泄漏。
11、 简述一下 C++ 中的多态
多态是面向对象编程中的一个重要概念,它允许不同的对象对同一消息做出不同的响应。C++ 中实现多态性的主要机制是通过虚函数(virtual function)和动态绑定(dynamic binding)来实现的。
1. 虚函数:
虚函数是在基类中声明为虚函数的函数。在派生类中,可以选择覆盖(重写)基类中的虚函数。虚函数使得在运行时可以动态地确定调用哪个版本的函数,而不是在编译时静态地确定。
class Shape {
public:
virtual void draw() {
// 基类中的虚函数
}
};
class Circle : public Shape {
public:
void draw() override {
// 派生类中的覆盖虚函数
}
};
2. 动态绑定:
动态绑定(Dynamic Binding)是面向对象编程中多态性的一个重要概念。它指的是在运行时确定调用哪个函数版本的过程,而不是在编译时确定。动态绑定可以确保在继承关系中正确调用子类的函数,实现多态性。
在 C++ 中,动态绑定通常通过虚函数来实现。虚函数是在基类中声明为虚拟的函数,这样子类可以覆盖(重写)该函数,并且在运行时确定要调用哪个版本。
以下是一个简单的示例,演示了动态绑定的用法
#include <iostream>
class Animal {
public:
virtual void makeSound() {
std::cout << "Animal makes a sound" << std::endl;
}
};
class Dog : public Animal {
public:
void makeSound() override {
std::cout << "Dog barks" << std::endl;
}
};
class Cat : public Animal {
public:
void makeSound() override {
std::cout << "Cat meows" << std::endl;
}
};
int main() {
Animal* animalPtr;
Dog myDog;
Cat myCat;
animalPtr = &myDog;
animalPtr->makeSound(); // 动态绑定,调用 Dog 类的 makeSound 函数
animalPtr = &myCat;
animalPtr->makeSound(); // 动态绑定,调用 Cat 类的 makeSound 函数
return 0;
}
3. 多态性:
多态性允许以一致的方式处理不同类型的对象。通过将对象指针或引用指向其基类,可以使用基类的接口来调用对象的方法,而实际执行的是派生类中的方法。
int main() {
Shape* shapes[2];
shapes[0] = new Circle();
shapes[1] = new Square();
for (int i = 0; i < 2; ++i) {
shapes[i]->draw(); // 动态绑定,根据实际对象的类型调用相应的函数
}
return 0;
}
12、多态分类
多态可以分为两种主要类型:编译时多态(静态多态)
,和运行时多态(动态多态)
。
1. 编译时多态(静态多态):
编译时多态发生在编译阶段,是通过函数重载和运算符重载来实现的。在编译时多态中,函数调用被解析为特定的函数实现,而这个解析是在编译时完成的。
1.1 函数重载:
class CompileTimePoly {
public:
// 函数重载
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
};
1.2 运算符重载:
class Complex {
public:
int real, imag;
// 运算符重载
Complex operator+(const Complex& other) {
Complex result;
result.real = this->real + other.real;
result.imag = this->imag + other.imag;
return result;
}
};
2. 运行时多态(动态多态):
运行时多态是通过虚函数和动态绑定来实现的。在运行时多态中,函数的调用是在运行时动态确定的,可以根据对象的实际类型调用相应的函数。
2.1 虚函数:
class Shape {
public:
// 虚函数
virtual void draw() {
// 基类中的虚函数
}
};
class Circle : public Shape {
public:
void draw() override {
// 派生类中的覆盖虚函数
}
};
2.2 动态绑定:
void drawShape(Shape* shape) {
shape->draw(); // 动态绑定,根据实际对象的类型调用相应的函数
}
int main() {
Circle circle;
drawShape(&circle); // 调用派生类中的 draw 函数
return 0;
}
运行时多态性使得在运行时能够动态地确定调用的函数版本,从而在运行时实现灵活的对象行为。
12、 说说为什么要虚析构,为什么不能虚构造、
在C++中,虚析构函数(virtual destructor)是为了正确释放动态分配的内存而引入的。虚析构函数在基类中声明为虚函数,而在派生类中进行覆盖。这样可以确保在使用基类指针删除指向派生类对象的情况下,能够调用正确的析构函数,从而避免内存泄漏。
为什么要虚析构函数:
1、正确释放资源: 如果在基类中不声明虚析构函数,当使用基类指针删除指向派生类对象的对象时,只会调用基类的析构函数,而不会调用派生类的析构函数。这可能导致派生类中的资源无法正确释放。
2、多态销毁: 允许通过基类指针删除派生类对象,实现多态性。如果析构函数不是虚的,将无法正确调用派生类的析构函数,导致对象的析构行为不符合预期
class Base {
public:
virtual ~Base() {
// 虚析构函数
}
};
class Derived : public Base {
public:
~Derived() override {
// 派生类中的析构函数
}
};
至于为什么不能有虚构造函数,主要是因为在对象构造过程中,虚函数表(vtable)
尚未建立。虚函数表是在对象构造期间进行初始化的,因此虚构造函数的概念是不合理的。虚构造函数会在对象构造的过程中引起矛盾,因此在C++中没有虚构造函数这个概念。
虚构造函数的需求通常通过设计模式(如工厂模式)或其他方法来满足,而不是通过语言层面的虚构造函数。
13 说说模板类是在什么时候实现的
在C++中,模板类的实现通常是在头文件(.h或.hpp文件)中完成的。模板类的定义和实现都包含在头文件中,以便在程序的各个地方都能够使用模板。
模板类的实现通常包含在头文件中的两个部分:
模板类的声明: 包括类的定义和成员函数的声明,但不包括具体的实现。
// MyClass.h
#ifndef MYCLASS_H
#define MYCLASS_H
template <typename T>
class MyClass {
public:
MyClass(T value);
void printValue();
private:
T data;
};
#endif
模板类的实现: 包括成员函数的具体实现。
// MyClass.hpp
#ifndef MYCLASS_HPP
#define MYCLASS_HPP
#include <iostream>
template <typename T>
MyClass<T>::MyClass(T value) : data(value) {}
template <typename T>
void MyClass<T>::printValue() {
std::cout << "Value: " << data << std::endl;
}
#endif
使用模板类的代码需要包含头文件,并在需要使用模板类的地方实例化模板,例如
#include "MyClass.hpp"
int main() {
MyClass<int> intObj(42);
intObj.printValue();
MyClass<double> doubleObj(3.14);
doubleObj.printValue();
return 0;
}
由于模板类的实现通常在头文件中完成,因此每次使用模板类时,编译器会根据实际的模板参数生成对应的代码。这种方式允许在编译时对模板进行具体化,从而在不同的上下文中使用相同的模板类。
14 、说说类继承时,派生类对不同关键字修饰的基类方法的访问权限
在C++中,派生类对基类的方法的访问权限取决于这些方法在基类中的访问权限以及派生类对这些方法的覆盖方式。主要涉及到 public、protected 和 private 这三个访问修饰符。
. public 继承:
1.1 基类方法为 public:
public 成员:在派生类中仍然是 public。
protected 成员:在派生类中变为 protected。
private 成员:在派生类中无法访问
class Base {
public:
void publicMethod() {}
protected:
void protectedMethod() {}
private:
void privateMethod() {}
};
class Derived : public Base {
// publicMethod 在 Derived 中仍然为 public
// protectedMethod 在 Deriv7ed 中变为 protected
// privateMethod 在 Derived 中无法访问
};
在C++中,派生类对基类的方法的访问权限取决于这些方法在基类中的访问权限以及派生类对这些方法的覆盖方式。主要涉及到 public、protected 和 private 这三个访问修饰符。
1. public 继承:
1.1 基类方法为 public:
public 成员:在派生类中仍然是 public。
protected 成员:在派生类中变为 protected。
private 成员:在派生类中无法访问。
class Base {
public:
void publicMethod() {}
protected:
void protectedMethod() {}
private:
void privateMethod() {}
};
class Derived : public Base {
// publicMethod 在 Derived 中仍然为 public
// protectedMethod 在 Derived 中变为 protected
// privateMethod 在 Derived 中无法访问
};
2. protected 继承:
2.1 基类方法为 public:
public 成员:在派生类中变为 protected。
protected 成员:在派生类中变为 protected。
private 成员:在派生类中无法访问
class Base {
public:
void publicMethod() {}
protected:
void protectedMethod() {}
private:
void privateMethod() {}
};
class Derived : protected Base {
// publicMethod 在 Derived 中变为 protected
// protectedMethod 在 Derived 中变为 protected
// privateMethod 在 Derived 中无法访问
};
3. private 继承:
3.1 基类方法为 public:
public 成员:在派生类中变为 private。
protected 成员:在派生类中变为 private。
private 成员:在派生类中无法访问。
class Base {
public:
void publicMethod() {}
protected:
void protectedMethod() {}
private:
void privateMethod() {}
};
class Derived : private Base {
// publicMethod 在 Derived 中变为 private
// protectedMethod 在 Derived 中变为 private
// privateMethod 在 Derived 中无法访问
};
总的来说,继承中的访问权限主要受到基类成员的访问权限以及派生类对这些成员的覆盖方式的影响。这是为了保证派生类对象在使用基类接口时能够维持基类的接口一致性
15 简述一下移动构造函数,什么库用到了这个函数?
移动构造函数是C++11引入的一项特性,用于实现对象的资源移动而非复制,从而提高性能。它通过使用右值引用(Rvalue Reference)来实现,允许在对象之间转移资源的所有权。
class MyClass {
public:
// 移动构造函数
MyClass(MyClass&& other) noexcept {
// 进行资源的移动操作,而非拷贝
}
};
移动构造函数是C++11引入的一项特性,用于实现对象的资源移动而非复制,从而提高性能。它通过使用右值引用(Rvalue Reference)来实现,允许在对象之间转移资源的所有权。
class MyClass {
public:
// 移动构造函数
MyClass(MyClass&& other) noexcept {
// 进行资源的移动操作,而非拷贝
}
};
移动构造函数通常与右值引用一起使用。右值引用是通过使用 && 来声明的,表示该引用可以绑定到右值(临时对象、将要销毁的对象等)。移动构造函数允许将其他对象的资源(比如堆上的内存)“窃取”过来,而不是复制。
库用到移动构造函数:
移动构造函数在处理大型数据结构、动态分配内存、I/O 操作等场景中可以显著提高性能,尤其是当对象包含资源(如指针)时。一些标准库中的容器、智能指针和字符串类等,都充分利用了移动构造函数以提高性能。
1. 标准库容器:
例如,std::vector 在动态增长时可能会涉及到内存重新分配,移动构造函数能够在元素移动时实现资源的高效转移。
std::vector source = getVector();
std::vector destination = std::move(source); // 移动构造函数
2. 智能指针:
智能指针类如 std::unique_ptr 和 std::shared_ptr 也使用了移动语义,以避免昂贵的资源复制
std::unique_ptr<int> source = std::make_unique<int>(42);
std::unique_ptr<int> destination = std::move(source); // 移动构造函数
3. 字符串类:
std::string 类也使用了移动构造函数,以在字符串复制时实现资源的移动而非复制。
std::string source = "Hello, World!";
std::string destination = std::move(source); // 移动构造函数
这些例子中,std::move 是一个将左值转换为右值的工具,用于显式地触发移动操作。移动构造函数的使用可以减少对象复制的开销,提高程序性能。
16 构造函数为什么不能被声明为虚函数?
构造函数不能被声明为虚函数的主要原因涉及到虚函数的特性和对象构造的过程。
1、虚函数的调用是通过虚函数表(vtable)来实现的。 虚函数表是在对象构造期间被初始化的,而对象的构造是在构造函数中完成的。因此,在对象的构造过程中,虚函数表还没有准备好,所以构造函数不能是虚函数。
2、虚函数表的建立是在对象的构造过程的最后阶段完成的,因此在构造函数中无法访问虚函数表。如果将构造函数声明为虚函数,那么在构造对象的过程中,虚函数调用将无法正常工作,因为此时虚函数表还没有被正确地初始化。
3、另外,析构函数可以声明为虚函数,因为虚析构函数允许在派生类的对象被删除时正确调用派生类特定的析构函数。这是因为在对象的销毁过程中,虚函数表仍然是有效的。但是,在构造函数中,虚函数表尚未准备好,因此无法声明构造函数为虚函数。
总之,虚函数的调用依赖于虚函数表,而在对象构造期间,虚函数表尚未被初始化,因此构造函数不能被声明为虚函数。
17 简述一下什么是常函数,有什么作用
常函数(const member function)是指在类的成员函数声明或定义中使用 const 关键字修饰的函数。常函数的主要作用是表明该成员函数不会修改对象的成员变量。
class MyClass {
public:
void regularFunction() {
// 可以修改对象的成员变量
}
void constFunction() const {
// 不可以修改对象的成员变量
}
};
在常函数中,对于对象的成员变量,只能进行读取操作,而不能进行修改。这有助于确保常函数对对象的状态没有影响,提高了代码的可读性和可维护性。
常函数的作用:
1、对象状态不变性: 常函数强调它不会修改对象的状态。这对于类的使用者来说是一种承诺,可以使代码更易于理解和推理。
2、与 const 对象配合使用: 常函数在处理 const 对象时非常有用。只有常函数才能被 const 对象调用,因为 const 对象要求不会修改对象的成员
const MyClass myConstObject;
myConstObject.constFunction(); // 可以调用常函数
// myConstObject.regularFunction(); // 错误,不能调用非常函数
3、避免意外修改: 常函数的使用可以防止在对象的常量上调用函数时发生意外的修改。
void someFunction(const MyClass& obj) {
obj.constFunction(); // 可以调用常函数
// obj.regularFunction(); // 错误,不能调用非常函数
}
总的来说,常函数是一种编程实践,有助于提高代码的可靠性,降低错误发生的概率。
18、 说说什么是虚继承,解决什么问题,如何实现?
虚继承是C++中的一种继承方式,用于解决多重继承中可能引发的“菱形继承”问题。菱形继承(Diamond Inheritance)是指一个类同时继承自两个间接共同继承的基类,导致了二义性和资源浪费的问题。
考虑以下继承关系
A
/ \
B C
\ /
D
如果B和C都继承自A,而D同时继承自B和C,这就形成了一个菱形继承结构。在这种情况下,D会继承来自A的两份副本,可能导致二义性和资源浪费。
虚继承通过使用关键字 virtual 来解决这个问题。在虚继承中,基类被标记为虚基类,从而在派生类中只维护一个共享的基类实例。
class A {};
class B : public virtual A {};
class C : public virtual A {};
class D : public B, public C {};
在这个例子中,B 和 C 都被声明为虚基类,而 D 继承自 B 和 C。通过虚继承,D 只包含一个共享的 A 实例,避免了菱形继承问题。
解决的问题:
1、二义性解决: 虚继承消除了菱形继承结构中可能导致的二义性问题。如果不使用虚继承,D 将包含两个独立的 A 的副本,导致不明确的访问。
2、资源浪费减少: 虚继承减少了资源的浪费,因为共享的基类实例只有一份,而不是多份。
实现:
在实现中,虚继承通过在派生类对基类的继承声明中使用 virtual 关键字来完成。这样做会影响派生类的布局,因为它需要一个额外的指针或偏移量来访问虚基类。
需要注意的是,虚继承虽然解决了一些问题,但也引入了一些复杂性,包括派生类构造函数的调用顺序等。因此,使用虚继承时需要慎重考虑,并根据实际情况权衡利弊
19 简述一下虚函数和纯虚函数,以及实现原理
1、虚函数(Virtual Function):
虚函数是在基类中声明为虚函数的函数,它可以在派生类中被重写(覆盖)。使用虚函数的关键字是 virtual。虚函数的调用是在运行时进行的,通过虚函数表(vtable)来实现
class Base {
public:
virtual void foo() {
// 虚函数
}
};
class Derived : public Base {
public:
void foo() override {
// 重写基类的虚函数
}
};
2、纯虚函数(Pure Virtual Function):
纯虚函数是在基类中声明为纯虚函数的函数,它没有具体的实现,并且要求派生类必须提供实现。使用纯虚函数的关键字是 virtual 后跟 = 0。
class AbstractBase {
public:
virtual void pureVirtualFunction() = 0; // 纯虚函数
};
类含有纯虚函数的类被称为抽象类,不能被实例化。派生类必须实现所有纯虚函数才能成为具体类。
1、实现原理:
(1)、虚函数的实现依赖于虚函数表(vtable)的概念。每个含有虚函数的类都有一个虚函数表,其中存储了该类的虚函数的地址。派生类的虚函数表包含了基类的虚函数表,以及派生类新增的虚函数。
(2)、当通过基类指针或引用调用虚函数时,实际调用的是对象的类型所对应的虚函数表中的函数。这种调用方式称为动态绑定,它使得在运行时能够根据实际对象的类型动态地确定调用的函数。
(3)、纯虚函数的实现同样依赖于虚函数表,但是派生类必须提供对应的实现,否则它也将成为抽象类,无法被实例化。
虚函数和纯虚函数的引入使得C++具备了面向对象编程中的多态性,允许在运行时选择正确的函数版本,提高了代码的灵活性和可维护性。
20、 说说纯虚函数能实例化吗,为什么?派生类要实现吗,为什么?
纯虚函数本身不能实例化。类中包含纯虚函数的类被称为抽象类,抽象类不能直接被实例化。这是因为抽象类中的纯虚函数没有具体的实现,因此无法构造一个完整的对象
class AbstractBase {
public:
virtual void pureVirtualFunction() = 0; // 纯虚函数
};
// 以下代码将无法通过编译,因为抽象类 AbstractBase 不能被实例化
// AbstractBase obj; // 错误
派生类必须提供对应的实现来使抽象类变得具体化。如果派生类没有提供对应的实现,它也将成为抽象类,无法被实例化。
class ConcreteDerived : public AbstractBase {
public:
void pureVirtualFunction() override {
// 提供对应的实现
}
};
ConcreteDerived obj; // 正确,ConcreteDerived 是具体类
理论上,任何派生自抽象类的类,都必须实现所有的纯虚函数,否则它也将成为抽象类。这是为了确保所有的派生类都能够提供完整的实现,使得抽象类的接口能够在不同的派生类中得到具体的实现。
总的来说,纯虚函数的主要目的是定义一个接口,并留给派生类提供具体实现。抽象类是一种提供接口的方式,但不能被实例化,而具体类(即提供所有纯虚函数实现的派生类)可以被实例化。