目录
1、指针与引用的区别
a.指针需要额外的地址来存放自己,而引用则不需要额外的内存,他只是变量的别名。
b.指针指向的地址可以改变,而引用初始化后就不能改变。
c.指针可以为空,而引用必须在声明的时候初始化。
2.const关键字
被他修饰的值不能改变,必须在定义的时候赋初值。
常量指针和指针常量
数据类型 const* 指针变量 是常量指针的形式,其指向的地址的值不能改变。
数据类型 *const 指针变量 是指针常量的形式,其指向的地址不能改变。
3.重载和重写(覆盖)的区别
重写一般用于子类继承父类时,重写父类的虚函数方法。override关键字是为了确保父子类的虚函数参数一致,否则弹出错误。
重载存在于一个类中,方法名称相同,而参数形式不同。
重写方法的参数列表,返回值要与被重写的一致。
静态方法不能被重写为非静态。因为静态方法隶属于类,而非静态方法属于对象,重写是基于对象的多态性实现的,所以不行。
被重写的方法不能为私有方法。私有方法不能被继承,这样会破坏其私有性。
4.new和malloc的区别(new封装了malloc)
new分配内存失败时,会抛出异常,而malloc则是返回NULL。
new申请内存分配时,无需指定大小,而malloc则需要显式的指出。
new/delete支持重载,而malloc/free则不支持。
new/delete会调用对象的构造函数和析构函数,而malloc不会
new从自由存储区上分配内存,malloc从堆上分配
5.static和const的区别
const修饰的变量,超出其作用域后会被释放,同时在定义时必须初始化,之后无法更改。而static在函数执行之后不会立即释放。static修饰成员函数称为静态成员函数,不依赖于任何实例,可以通过类名直接调用。因为其没有this指针,不能访问非静态成员变量或函数。同时不能声明为virtual,因为虚函数的调用是通过对象的虚函数表来实现的(里面存的指向各个虚函数的指针),而静态成员函数是与类相关联,而不是与类的对象相关联的。
- static用于声明静态变量和静态函数,它们在内存中只有一份拷贝,与任何对象实例无关。
6. c++三大特性
在类的外部,只能通过对象访问public属性的成员。
继承:可以使用现有类的所有功能,并在无需重新编写原有类的情况下对这些功能进行扩充。
封装:把客观事物抽象成类,并对信息进行分享和隐藏。
多态:允许子类类型的指针赋值给父类类型的指针。重载实现编译多态,虚函数实现运行时多态。实现多态有两种方式,覆盖:子类重新定义父类的虚函数的做法。重载:多个同名函数,参数表不同。
class Base {
public:
virtual void foo() {
cout << "Base::foo()" << endl;
}
};
class Derived : public Base {
public:
void foo() override { // 重写了基类的虚函数foo()
cout << "Derived::foo()" << endl;
}
};
Base* ptr = new Derived();
ptr->foo(); // 会调用Derived::foo()
7.虚函数
当基类希望派生类定义适合自己的版本,就将这些函数声明成虚函数。虚函数是动态绑定的,基类的指针指向谁的类,查询谁的类的虚函数表,这个机制保证派生类的虚函数被调用到。
虚函数实现多态:调用函数的对象必须是指针或者引用,同时被调用的函数必须是虚函数,且完成了虚函数的重写。
构造函数不能是虚函数,因为构造函数在对象被创建时调用,虚函数需要根据对象的动态类型来确定调用哪个函数版本,对象都没被创建完成,又怎么知道他的类型那。
析构函数可以是虚函数,当这个类作为基类实现多态时,基类指针指向派生类的对象,这时为了析构派生类对象,基类的析构函数必须是虚函数。
内联函数不能为虚函数,因为inline在编译时并不知道对象的类型,而虚函数则需要知道对象的类型才知道调用哪个虚函数,所以没法在编译时进行内联函数展开。这里再讲一下inline函数,他一般写在函数定义和声明的前面,是为了解决一些频繁调用的小函数而大量消耗栈内存的问题。一般用于小的频繁调用的函数体。inline函数只是对编译器的一个建议,如果它感觉函数太大了,也可以不作为内联函数使用。
inline int add(int x, int y); //声明时
inline int add(int x, int y) {
return x + y;
}//定义时
静态函数也不能成为虚函数,因为静态成员没有this指针,而虚函数一定要通过对象来调用,有隐藏的this指针。
8.纯虚函数
纯虚函数是在基类中声明的,没有具体实现的虚函数,它只提供接口,需要派生类自己实现。使用纯虚函数的类是抽象类,不能实例化。所以派生类必须重写纯虚函数,不然他也是抽象类,不能实例化。
class AbstractBase {
public:
virtual void pureVirtualFunc() = 0; // 纯虚函数声明
};
class Derived : public AbstractBase {
public:
void pureVirtualFunc() override {
// 派生类提供的纯虚函数实现
}
};
AbstractBase* ptr = new Derived();
ptr->pureVirtualFunc(); // 调用派生类对象的纯虚函数实现
delete ptr; // 删除派生类对象
9.虚继承
虚继承是为了解决多继承时的菱形继承问题,防止出现二义性。
//间接基类A
class A{
protected:
int m_a;
};
//直接基类B
class B: virtual public A{ //虚继承
protected:
int m_b;
};
//直接基类C
class C: virtual public A{ //虚继承
protected:
int m_c;
};
//派生类D
class D: public B, public C{
public:
void seta(int a){ m_a = a; } //正确
void setb(int b){ m_b = b; } //正确
void setc(int c){ m_c = c; } //正确
void setd(int d){ m_d = d; } //正确
private:
int m_d;
};
int main(){
D d;
return 0;
}
10. 智能指针
智能指针是为了解决动态分配内存导致内存泄漏和多次释放同一内存所提出的。放在<memory>头文件。包括共享指针,独占指针,弱指针
shared_ptr:是一种引用计数的智能指针,其通过一个计数器来追踪有多少个智能指针共享同一资源,当为计数为0时,释放资源。也就是当没有智能指针指向这个资源时,这个资源会被释放。
他的多线程在只读的情况下是安全的,如果是多个线程同时修改引用计数,就需要额外的同步来保证线程安全,否则,在并发修改引用计数时可能会导致数据竞争和未定义行为。
#include <memory>
std::shared_ptr<int> ptr1 = std::make_shared<int>(42); //第三种构造方式
std::shared_ptr<int> ptr2 = ptr1; // 共享所有权
'''
这两种不常用
int* a = new int(4);
std::shared_ptr<int> ptr3(a); //第一种构造方式
std::shared_ptr<int> ptr1(new int); //第二种构造方式
'''
只有ptr1和ptr2都被销毁时,才释放int型资源。
unique_ptr:是一种独占所有权的智能指针,不能与其他共享管理权,也就是不支持普通的拷贝和赋值,当其销毁时,它管理的资源也会销毁。
#include <memory>
std::unique_ptr<int> ptr = std::make_unique<int>(42);
weak_ptr :是为了配合shared_ptr而引入的,像旁观者那样观察资源的使用情况,但它没有共享资源,他的构造不会引起指针引用计数增加。
11. 内存泄漏
堆内存泄露:是指程序中使用malloc和new等从堆中分配一块内存,用完之后忘记用free或delete删除。
系统资源泄露:指程序使用系统分配的资源,但并没有调用相应的函数释放掉,导致系统资源浪费。
没有将基类的析构函数定义为虚函数:当基类的指针指向子类的对象时,如果基类的析构函数不是虚函数,那么子类的析构函数将不会被调用,造成内存泄漏。
如何防止内存泄漏?将内存的分配封装在类中,构造函数分配内存,析构函数释放内存。使用智能指针。
12.c++的内存分布
栈区存放的局部变量和函数的参数值。从高地址到低地址。
堆区存放的动态申请的内存。
全局区存放的全局变量和静态变量。在程序编译时内存就已经分配好了
13.STL介绍
容器:各种数据结构,如pair,vector,list,deque,set,map等。
适配器:queue和stack,他们的底层都是deque实现的。
pair:里面的元素调用a->first, a->second,插入pair时,insert({first, second})或insert(pair<类型,类型>(数据1,数据2));
vector:在堆中分配了一段连续的内存空间。他的扩容是每次翻倍,容量不够就翻倍。capacity永远大于size。底层实现是数组。
操作:push_back,emplace_back,pop_back, clear,
insert(position, value)
:在指定位置插入一个元素。erase(position)
:移除指定位置的元素。erase(first, last)
:移除指定范围内的元素
list:内存空间是不连续,通过指针来进行数据的访问。通常用来随即插入删除操作。是双向链表。
deque:双端队列,由于需要内部跳转,所以随机访问没有vector快。由于deque的迭代器要比vector的复杂,对deque排序时,可以先把deque复制到vector里再排序,再放回deque。操作:push_back,pop_back,push_front,pop_front
stack和queue被称为适配器,底层是deque。操作:push,pop,empty,size。此外队列还有front,back,栈还有top
map和set:底层采用红黑树实现,查找的复杂度为log(n).
unordered_map和unordered_set:底层实现是哈希表,查找复杂度为1
14.enable shared from this是什么意思?
在某些情况下,我们需要把对象自身作为一个智能指针进行管理。通常出现在对象内部存在其他对象或函数,他们需要引用当前对象,如果使用裸指针传递或拷贝对象,会出现资源管理的困难。
"enable_shared_from_this" 是一个基类,主要用于解决对象自身作为 shared_ptr 资源的情况下的共享资源管理问题。这个基类定义了成员函数shared_from_this(),通过这个函数可以返回一个指向当前对象的shared_ptr,确保正确地管理和共享资源。
#include <iostream>
#include <memory>
class MyClass : public std::enable_shared_from_this<MyClass> {
public:
std::shared_ptr<MyClass> getShared() {
return shared_from_this();
}
};
int main() {
std::shared_ptr<MyClass> obj1 = std::make_shared<MyClass>();
std::shared_ptr<MyClass> obj2 = obj1->getShared();
std::cout << "obj1 use count: " << obj1.use_count() << std::endl;
std::cout << "obj2 use count: " << obj2.use_count() << std::endl;
return 0;
}
上述样例中Myclass继承了基类enable_shared_from_this,并定义了一个成员函数getshared(),该函数通过调用shared_from_this()返回一个指向当前对象的智能指针。
15.this指针的用处
当我们在类中定义了一个变量,同时又在类的成员函数中定义了一个同样的变量,此时想要用类中的变量时,就需要this指针。
当类的非静态成员函数返回类对象本身时,直接使用return *this
this指针的作用域是在类内部,只能在成员函数中使用,静态成员函数和全局的不行。
16.左值右值以及相关操作
左值是指表达式结束后仍然存在,具有具体的内存地址等。
右值是指表达式结束后不再存在的临时对象
int a = 5; // 5 是一个右值
int b = a + 3; // a + 3 是一个右值表达式
c++11引入了右值引用的概念,使得右值也可以被引用,并且可以绑定到右值,使得可以对其进行移动语义和移动构造。右值引用允许我们有效地实现转移语义、完美转发和移动语义等高效的操作。
int&& rref = 42; // 右值引用绑定到右值
std::move()存在于<utility>库中,他的作用是将一个左值转换为一个右值引用,将左值强制转换为右值,从而触发移动语义和完美转发。
#include <utility>
void func(int&& value) {
// 对右值引用的参数进行操作
}
int main() {
int x = 42; // x 是一个左值
int&& rref = std::move(x); // 使用 std::move 将 x 转换为右值引用
func(std::move(x)); // 将 x 转换为右值引用并传递给函数
return 0;
}
std::forward是将一个参数进行传递,并保存其原始值类型,如左值和右值。一般只用在模板函数中,以便完成完美转发。在普通函数中使用std::forward是没有意义的。模板函数可以根据不同类型的参数生成多个具体的函数实例。模板函数的定义使用关键字 template
和模板参数列表来指定模板参数,下面跟一个函数。
#include <utility>
// 使用 std::forward 完美转发参数
template<typename T> //模板函数
void wrapper(T&& arg) { //和万能引用放在一起,这样T既可以接收左值,也可以接收右值。
other_function(std::forward<T>(arg)); // 将参数 arg 以其原始的值类别(左值引用或右值引用)传递给其他函数
}
void other_function(int& x) {
// 处理左值引用参数
}
void other_function(int&& x) {
// 处理右值引用参数
}
int main() {
int x = 42;
wrapper(x); // 调用传递左值引用的版本
wrapper(123); // 调用传递右值引用的版本
return 0;
}
移动构造函数:传进来的是右值引用,这样可以避免不必要的内存拷贝。并将源对象的资源指针或状态指针移动到目标对象中,同时将源对象置于有效但未定义的状态,以便析构时能正确处理资源的释放。移动构造函数通常应该标记为 noexcept
,以确保在对象移动期间不会抛出异常。这样可以使编译器对代码进行优化,提高性能。
移动语义是指在对象传递或赋值时,将资源从一个对象转移到另一个对象,而不进行深拷贝操作。移动操作比拷贝操作更高效,尤其是在处理大型对象或持有大量资源的对象时。移动语义通过移动构造函数和移动赋值运算符来实现。
转移语义是指通过转移操作(如 std::move)将一个对象标记为可转移的右值引用(Rvalue Reference),以便在移动操作中使用
17.vector里resize和reserve的区别
resize改变vector的大小,会增加和删除vector里的元素。而reserve不改变vector原来的值,只是用于预分配其空间大小,避免频繁的内存分配。
18.vector和list的区别
vector里的元素是顺序存储,所以内存不够需要扩容。而list的底层实现是双向链表,其内存是离散分布的。
由于是双向链表的结构,所以list在任意位置插入和删除的时间复杂度为o(1),但其无法通过下标访问元素,只能遍历来找到元素。vector可以通过下标访问元素o(1),但是插入删除为o(n).