四、面向对象
1.什么是面向对象
面向对象是种编程思想,把一切东西看成一个对象,这个对象拥有自己的属性,我们把对象拥有的属性变量和操作这些属性变量的函数打包成一个类来表示,而类可以实例化出对象。
2.面向对象和面向过程的区别
**面向过程:**面向过程以事件或流程为中心,强调问题解决的步骤,关注函数或方法的调用。它主要分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用。
**面向对象:**以对象为中心,将问题分解成一个个对象,通过不同对象之间的调用和相互协作来解决问题;它关注的是对象的属性和方法,以及对象之间的交互关系。
3.面向对象的三大特征
继承、封装、多态
封装:
将数据和操作数据的方法有机结合,隐藏对象的属性和实现细节,仅仅对外提供接口来和对象进行交互,封装让模块有较好独立性,让程序维护修改更容易。
继承:
对象的一个新类可以从现有的类中派生,这个就是继承,新类保留了原始类的特性,新类叫做派生类,或叫子类,原始类叫父类或叫基类,继承很好的解决了代码可重用性问题,在子类里面可以添加属于自己的新的函数或者变量。
继承方式:
公有、私有、保护
对于基类的私有成员无论子类是以哪种方式继承都无法操作基类的私有成员;
对于私有继承,会把基类的保护成员、公有成员变为私有成员;
对于保护继承,会把基类的保护成员、公有成员变为保护成员。
多态:
用父类的指针指向子类的实例,然后通过父类的指针调用子类的成员函数,一般有重写、重载
重写是动态多态,重载是静态多态(编译器在编译期完成)
重写需要满足条件:
1)虚函数,基类中必须有虚函数,在派生类中必须重写虚函数;
2)通过基类类型的指针或引用来调用虚函数。
4.什么是深拷贝?什么是浅拷贝?
对一个已知对象进行拷贝,编译系统会自动调用一种构造函数——拷贝构造函数,如果用户未定义拷贝构造函数,则会调用默认拷贝构造函数。
编译系统在我们没有自己定义拷贝构造函数时,会在拷贝对象时调用默认拷贝构造函数,进行的是浅拷贝,(释放时,如果里面有指针就会出现不同对象对同一个内存进行多次释放,报错),在对含有指针成员的对象进行拷贝时,必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存出现两次释放发生。
总结:
浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,
深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。
如何区分深拷贝与浅拷贝?
简单点来说,就是假设B复制了A,当修改A时,看B是否会发生变化,如果B也跟着变了,说明这是浅拷贝; 如果B没变,那就是深拷贝!
5.什么是友元函数,友元类?
友元函数和友元类是计算机科学中的概念,尤其在面向对象编程中扮演重要角色。它们提供了一种机制,使得非成员函数或非类能够访问类的私有(private)或保护(protected)成员。
友元函数是指虽然不是类的成员函数,但却能够访问类的所有成员(包括私有和保护成员)的函数。这些函数定义在类的外部,但需要在类的内部声明为友元函数。虽然友元函数可以访问类的私有成员,但它并不是类的成员函数,因此不能访问this指针。友元函数在某些特定情况下非常有用,例如运算符重载和类的拷贝构造函数等。
另一方面,友元关系也可以应用于类之间。当一个类被声明为另一个类的友元时,这个类的所有成员函数都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。这意味着,友元类中的所有成员函数都自动成为另一个类的友元函数。
6.什么函数不能声明为虚函数
普通函数(非成员函数),静态成员函数,内联成员函数,构造函数,友元函数。
7. vector和list的区别有什么
1、vector底层是数组,list是双链表;
2、vector支持随机访问,list不支持;
3、vector是顺序内存,list不同;
4、vecotr随机访问性能好,插入删除性能差,list相反;
5、vecor一次性分配好内存,不够才2被进行扩容,list每次插入一个节点都会进行内存申请。
8.说一说vector扩容时发生了什么
vector空间已满时会申请新的空间并将原容器中的内容拷贝到新空间中,并销毁原容器;
存储空间的重新分配会导致迭代器失效。
9.删除元素导致两者迭代器发生什么变化
1) 对于关联容器(如map, set,multimap,multiset),删除当前的iterator,仅仅会使当前的iterator失效,只要在erase时,递增当前iterator即可。这是因为map之类的容器,使用了红黑树来实现,插入、删除一个结点不会对其他结点造成影响。
2)对于序列式容器(如vector,deque),删除当前的iterator会使后面所有元素的iterator都失效。这是因为vetor,deque使用了连续分配的内存,删除一个元素导致后面所有的元素会向前移动一个位置。还好erase方法可以返回下一个有效的iterator。
3)对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种正确的方法都可以使用。
10.如何理解迭代器
迭代器类型主要支持两类,随机访问和双向访问,其中vector和deque支持随机访问,list, set, map等支持双向访问。
11.容器的迭代器时由什么组成的
容器的迭代器主要由指针或类似指针的对象组成,它们用于在容器中遍历并访问元素。迭代器提供了一种统一的方式来访问容器中的元素,无论容器的具体实现方式如何。
12.假如给你一个class,让你去用class去实现一个智能指针,你会怎么做?
智能指针主要用于自动管理动态分配的内存,确保在不再需要对象时能够正确地释放其内存,从而防止内存泄漏。
#include <iostream>
#include <algorithm>
#include <cassert>
template<typename T>
class SmartPtr {
private:
T* ptr;
public:
// 构造函数,用于初始化智能指针
explicit SmartPtr(T* p = nullptr) : ptr(p) {}
// 拷贝构造函数,禁用拷贝,防止浅拷贝导致的所有权问题
SmartPtr(const SmartPtr& other) = delete;
// 移动构造函数,允许移动语义
SmartPtr(SmartPtr&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr;
}
// 拷贝赋值运算符,禁用拷贝赋值
SmartPtr& operator=(const SmartPtr& other) = delete;
// 移动赋值运算符,允许移动赋值
SmartPtr& operator=(SmartPtr&& other) noexcept {
if (this != &other) {
delete ptr;
ptr = other.ptr;
other.ptr = nullptr;
}
return *this;
}
// 析构函数,释放内存
~SmartPtr() {
delete ptr;
}
// 解引用运算符重载
T& operator*() const {
assert(ptr != nullptr);
return *ptr;
}
// 箭头运算符重载
T* operator->() const {
assert(ptr != nullptr);
return ptr;
}
// 获取原始指针(不推荐直接使用)
T* get() const {
return ptr;
}
// 检查指针是否为空
explicit operator bool() const {
return ptr != nullptr;
}
};
13.STL中迭代器有什么用?有指针为何还有迭代器
STL(Standard Template Library,标准模板库)中的迭代器是一个非常重要的概念,它提供了一种通用的方式来访问和遍历容器中的元素。虽然指针也可以用来访问内存中的元素,但迭代器在STL中扮演了更为关键和灵活的角色。
以下是迭代器的主要用途和优点:
- 抽象化访问:迭代器为容器提供了一个统一的接口,使得我们可以不关心容器的具体实现细节,就能访问其元素。无论是数组、链表、树还是其他数据结构,迭代器都提供了类似的操作方式,如递增、递减、解引用等。
- 类型安全:使用迭代器比直接使用指针更安全。迭代器通常与特定的容器类型相关联,这有助于防止越界访问或其他类型错误。
- 范围操作:迭代器支持范围操作,这使得我们可以轻松地遍历和处理容器中的一部分或全部元素。例如,我们可以使用STL算法库中的函数,如
std::for_each
、std::find
等,这些函数都接受迭代器作为参数,以定义操作的范围。 - 自定义迭代器:STL允许我们创建自定义迭代器,以支持非标准容器的遍历。这使得迭代器具有很高的灵活性和可扩展性。
- 指针的局限性:虽然指针也可以用来访问内存中的元素,但它们并不总是适用于所有情况。例如,在某些容器中,元素可能不是连续存储的,此时直接使用指针可能会导致问题。此外,指针缺乏类型检查和范围检查,这可能导致越界访问或野指针等问题。
因此,尽管指针在某些情况下仍然很有用,但在STL中,迭代器提供了一种更安全、更灵活、更抽象的方式来访问和遍历容器中的元素。这使得我们可以更容易地编写出可维护,鲁棒性强的代码。
14.STL中迭代器是如何删除元素
对于序列容器vector和deque,使用erase后,后面的元素的迭代器都会失效,但是后面每个元素都会往前面移动一个位置,erase返回下一有效的迭代器;
对于关联容器map、set使用erase后,当前的元素的迭代器会失效,但是其结构是红黑树,删除当前元素不会影响到下一个元素的迭代器,所以在调用erase之前,记录下个元素的迭代器即可;
对于list,使用不连续分配的内存,并且他的erase方法也会返回下个有效的迭代器。
15. map和set有什么区别?区别又是怎么实现的?
map和set都是C++的关联容器,底层都是红黑树
区别:
map中的元素是键值对,set只是关键字的集合,所以set中元素只包含一个关键字,
set的迭代器是const,不允许修改元素的值,
map允许修改value,但不允许修改key。
原因:根据关键字来保证器有序性,如果修改key的话,那么首先删除改建,然后调节平衡,再依据修改后的建值,调节平衡,这样一来破坏map和set的结构,iterator失效。
16. STL的allocator有什么作用
STL(Standard Template Library,标准模板库)中的allocator类是一个用于管理内存分配的重要组件。它的主要作用包括:
- 内存分配与回收:allocator提供了分配和回收对象内存的方法。例如,allocate方法用于分配内存,而deallocate方法用于释放内存。这使得STL容器(如vector、list等)能够动态地调整其大小,以适应程序运行时的需求变化。
- 对象构造与析构:除了管理内存,allocator还可以在分配的内存上构造对象(使用construct方法)和析构对象(使用destroy方法)。这种将内存分配和对象构造分离的设计,允许先分配大块内存,只在需要时才真正执行对象构造函数,从而提供了更好的性能和更灵活的内存管理能力。
17.说一说红黑树
红黑树是一种自平衡的二叉搜索树,它通过在插入和删除操作时通过特定的操作来保持树的平衡,从而获得较高的查找性能。红黑树是在计算机科学中广泛使用的一种数据结构,尤其在需要高效查找、插入和删除操作的场景中。
18. STL中的map和unordered_map有什么区别
map是使用红黑树实现,unordered_map是使用hash表来完成映射功能,
map是按照operator < 比较判断元素是否相同,及比较元素的大小,然后选择一个合适位置插入书中,所以对map遍历的话是有序的,
unordered_map是计算元素的hash值,根据hash的值判断元素是否相同,所以对unordered_map遍历是无序的。
19.STL的resize和reserver的区别
resize改变容器含有元素的数量,比如:resize(15),原来的大小是10,那么使用resize之后就会增加5个为0的元素,
reserver改变容器的最大容量capacity,不会生成元素,如果改变之后容器容量大于当前的capacity,那么就会出现分配一个空间,把之前的元素全部盖被到新的空间中。
20.为什么基类的构造函数和析构函数不能被继承
1、构造函数主要任务是初始化对象的状态,如果构造函数可以被继承,那么子类可能会继承父类不相关的初始化程序,造成子类对象的不正确性;
2、析构函数主要任务是释放对象的分配资源,每个类有不同的变量,如果可以继承基类,那么子类将不能正确释放特定的资源,可能导致内存泄漏。
21.C++中如何阻止一个类被实例化
可以通过使用抽象类,或者将构造函数说声明为private,抽象类之所以能被实例化,是因为抽象类不能代表一类具体的事物,他是对多种相似的具体事物的共同特征中抽象。
22.纯虚函数指的是什么?
class 类名
{
virtual() = 0;
};
含有纯虚函数的类称为抽象类,抽象类不能生成对象,纯虚函数永远不会被调用,它们主要是用来统一管理子类对象。
23.C++中哪些情况只能初始化列表,而不能赋值?
在c++中赋值就是删除原值,赋予新值,初始化列表是开辟空间和初始化同时完成
1、类中的const ,reference(引用)成员变量时,只能初始化,
2、若成员类型是没有默认构造函数的类,只能使用初始化列表,
3、派生类在构造函数中要对自己的自身成员初始化,也要对继承过来的基类成员进行初始化,当基类没有默认构造函数的时候,通过在派生类的构造函数初始化列表中调用。
24. 没有参数的函数能不能被重载?
可以
class A
{
public:
void f()
{
cout << 1 << endl;
}
void f() const // 没有参数的函数也是我可以被重载的,只不过要想使用,那么对象也必须是const
{
cout << 2 << endl;
}
};
int main()
{
A a;
const A b;
a.f(); //1
b.f(); //2
return 0;
}
25.虚函数表和虚函数表指针的建立时间
虚函数表在编译时期创建,编译期间,编译器位每个类确定好了对应的虚函数表里的内容,在程序运行时,编译器会把虚函数表的首地址赋值给虚函数指针,也就是虚函数表指针是在对象创建时创建的,所以,虚函数表的建立时间是在编译时,而虚函数表指针的创建时间是在运行时。