C++面经

提纲参考大庆指针面试宝典

1.c语言和C++的区别

首先头文件命名风格不同

C语言的头文件命名风格为.h,而C++不带

C语言没bool类型

C语言没有函数重载而C++有

原因是,编译器在编译的时候会把C++的命名识别为函数名字+参数列表,而C的命名只有函数名
而函数重载是一种静态多态,是函数名相同但参数列表不同,所以C语言没有函数重载。

动态多态

动态多态就是运行时候的多态,也就是说只有在运行阶段才能确定执行哪个函数,而静态多态被称为编译阶段的多态,在编译的时候就确定执行哪个函数了
动态多态一般发生在父类指针或引用指向子类对象的时候,具体举例:比如现在有父类和多个子类,父类中定义了一个virtual函数,子类中分别重写了该函数。这个时候函数调用的时候传子类对象地址,用父类指针去接收它们的话,就直到具体调用哪个重写的函数了
动态多态是靠虚表来实现的,虚表中存储虚函数的地址,每个类都有一个虚表,每个对象都有一个虚表指针,在编译的时候会给虚表绑定地址。具体实现就是当父类指针指向子类对象的时候,子类的对象去访问它的虚表指针,虚表指针找到虚表,虚表中找到需要调用虚函数的地址

C是面向过程,C++是面向对象(封装、继承和多态)

C++中NULL就是数字0,而c中NULL是地址0

证明:写一个函数重载,实参传NULL,形参写int* 和int类型

2.函数重载问题

如上
函数重载遇到参数默认值

俩参的func和一个参的func,但第一个func中多了一个参数默认值,编译器蒙了,无法确定指定哪个函数

3.引用

首先说一下引用的特点

1.引用必须进行初始化,且不能初始化为空
2.其本质是一个指针常量,所以指向不能改变,就导致了它的引用关系不能改变

然后说一下引用的分类

1.引用分为左值引用、右值引用和万能引用,左值引用只能接收左值,右值引用只能接收右值,万能引用既能接收左值也能接收右值。
2.比如万能引用在拷贝构造传参的时候使用,右值引用在移动构造传参的时候使用。移动构造的调用时利用std::move()函数,把已经存在的对象由左值转为右值,这时候传参的时候就会被参数为右值引用的移动构造接收到
3.移动构造不是拷贝了一个对象赋值给了新对象,而是把新对象移动到了原来对象的资源上
c++ 复制代码
#include <iostream>


using namespace std;

class Person{
public:
 Person(string name,int age,int val){
     this->name=name;
     this->age=age;
     this->pwd=new int;
     *pwd=val;
 }
 Person(const Person& other){
     cout<<"拷贝构造"<<endl;
     this->name=other.name;
     this->age=other.age;
     this->pwd=new int;
     *pwd=*other.pwd;
 }
 Person(Person&& other){
     cout<<"移动构造"<<endl;
     this->name=other.name;
     this->age=other.age;
     this->pwd=other.pwd;
 }
 string name;
 int age;
 int* pwd;
};
int main(){

 Person p1("施易辰",22,123456);
 Person p2(p1);
 Person p3(move(p1));
 cout<<p1.name<<endl;
 return 0;
}

接着说一下引用的好处

1.在传参方面,和指针传参效果相同,但却比指针更加简洁,因为指针传参过来的时候需要先判空,防止访问空指针,而引用传参则不用。
2.引用类型作为函数的返回值,一般函数的返回值为匿名对象,也就时不可寻址的右值,但是使用引用传参,返回值时左值,避免了拷贝构造带来的开销,但是于此同时也存在问题,就时不能返回局部变量的引用。解决方案可以用static延长声明周期

然后说一下引用与指针的区别

1.引用必须初始化且不能初始化为空,但指针可以
2.不能改变引用关系,但是可以需改指针的指向
3.引用比指针更加简洁,也更安全
4.没有多级引用,但是又多级指针
5.sizeof引用为变量本身大小,而sizeof指针4/8

4.面向对象的三大特征

封装、继承和多态

1.封装:把对象的属性和行为封装到一个类里面,一般时属性私有化,行为或者说的方法公有化。
2.继承:继承可以使代码变得更加简介,子类继承基类,子类相当于对基类进行的扩展,并且继承也就动态多态发生的前提
3.多态:在继承关系中,子类重写了父类的虚函数,然后调用函数的时候,父类指针指向了子类的对象,通过虚表机制,找到虚函数进行调用

5.new和malloc的区别

首先从底层上来看

new会先调用malloc函数,再调用构造函数给成员变量进行赋值(而初始化参数列表才是给成员变量进程初始化)。

其次从返回值上看

new的返回值不需要强转,malloc返回的是void*类型,需要强转

然后从名字上来看

new是运算符,可以进行重载(重载是一种静态多态),malloc是C语言函数,不能进行重载(因为编译器在编译的时候会对C函数的命名只有函数名,没有参数列表,所以没有重载)

然后从参数上来看

new不需要传参,而malloc需要传具体开辟的空间大小

然后从申请空间失败后的结果上来看

new会抛出一个异常,如果把其放在try中,catch会捕获该异常。malloc会返回一个空指针
c++ 复制代码
//自定义异常类
#include <iostream>


using namespace std;
class MyException{
 string str;
public:
 MyException(string str):str(str){}
 string what(){
     return str;
 }
};
int cal(int a,int b){
 if(!b){
     //发现异常
     //创建异常对象
     MyException my("除数不能为0");
     //抛出异常对象
     throw my;
     cout<<"抛出异常对象之后的代码不会执行"<<endl;
 }
}
int main(){
 int a,b;cin>>a>>b;
 try{
     cal(a,b);
 }
 catch(MyException &e){
     cout<<e.what()<<endl;
 }
 return 0;
}

然后从释放空间调用的函数上来看

new用delete,malloc用free(可以讲一下delete的底层)

6.构造函数

首先说一下构造函数的作用

构造函数是在创建对象的时候给成员变量进行赋值的函数

构成

无返回值 函数名与类名相同

特点

1.构造函数和普通函数一样可以进行函数重载
2.如果在类中没有自定义构造函数,系统会提供一个默认的无参构造函数
3.构造函数时给成员变量进行赋值的,给成员变量进行初始化的是初始化列表
c++ 复制代码
#include <iostream>
#include <cstdlib>

using namespace std;

class Base{
 int a;
 int b;
public:
 Base(int _a,int _b):a(_a),b(_b){
     cout<<"Base 构造"<<endl;
 }
 ~Base(){
     cout<<"Base 析构"<<endl;
 }
};
class Child:public Base{
public:
 //成员变量初始化的顺序只与类中声明顺序有关,与初始化列表中的顺序无关
 Child(int _a,int _b):
 Base(_a,_b){
     cout<<"Child 构造"<<endl;
 }
 //当类中有类成员和const成员变量或者引用类型的成员变量时,由于它们必须进行初始化,所以一定要放在初始化参数列表当中
 ~Child(){
     cout<<"Child 析构"<<endl;
 }
};
int main(){
 Child c(1,2);
 return 0;
}

委托构造

在同一个类中,使用一个类的构造函数调用同一个类中另外一个构造函数来完成初始化工作
c++ 复制代码
#include <iostream>
#include <cstdlib>

using namespace std;

class A{
 int a,b;
public:
 A():A(0){}
 A(int a){
     this->a=a;
 }
 int get_a(){
     return a;
 }
};
int main(){
 A a;
 cout<<a.get_a()<<endl;
 return 0;
}

7.析构函数

析构函数是在对象即将被销毁之前,系统会自动调用析构函数,其作用是释放成员变量所指向的堆区空间

格式

~类名(){}
没有返回值,函数名为~+类名,没有参数列表,所以也就不存在函数重载

如果没有实现析构函数,系统会提供默认的析构函数

c++ 复制代码
#include <iostream>


using namespace std;
class A{
    int *p= nullptr;
public:
    A(int n){
        if(n) p=new int[n];
    }
    ~A(){
        cout<<"析构函数"<<endl;
        if(p) delete []p;
    }
};


int main(){
    A *a = new A(5);
    delete a;
    return 0;
}

8.拷贝构造

首先说一下拷贝构造的调用时机

1.用一个已经存在的对象去初始化一个新对象
2.函数传参的时候以值传递的方式
3.函数的返回值以值的方式返回,会拷贝一个匿名对象,为右值

拷贝构造的参数必须是引用类型

防止无限递归,前面加const是为了防止通过形参修改实参的值

如果用户定义了拷贝构造的值,系统讲不会提供其他构造函数

浅拷贝

系统默认提供的就是浅拷贝,浅拷贝只是简单的值传递,不会在堆区中额外开辟空间,也就是说如果对象中有成员变量指向了一段堆区空间,在浅拷贝过程中不会在意外开辟一段堆区空间,而是把该空间的地址直接赋值给要拷贝的成员变量。这样也就造成了安全隐患,就是对象在被销毁的时候,该空间会连续被释放,产生错误

深拷贝

深拷贝就是为了解决这种问题,它在面对成员变量指向堆区空间的拷贝问题时,会开辟一段新的堆区空间,然后把要拷贝的内容赋值到这段空间之中

也就是说如果成员变量在堆区开辟空间了,就不要使用系统默认提供的浅拷贝构造函数了。需要我们自己写一个深拷贝,给成员变量重新开辟空间

9.const关键字

首先const关键字可以修饰普通变量

表示该变量不可以修改
c++ 复制代码
const_cast<int*> p;//该可以将p的const类型取消,变成非const类型

然后const关键字还可以修饰指针变量

1.放在*的左边表示指向的内容不可以改变
c++ 复制代码
const int * p=&a;
int const * p=&a;
2.放在*的与右边表示指向不可以改变
c++ 复制代码
int * const p=&a;

然后const还可以修饰函数

1.该函数称为常函数,因为this指针一般是作为隐含参数在函数传参的时候传递过过来的,其类型为常指常量。但常函数的this指针是常量指针常量,表示指向不能改变且指向里面的内容也不能改变,所以常函数中不能修改成员变量。因为this指针的不匹配,所以常函数只能调用常函数,不能调用非常函数。
2.常函数和非常函数之间发生函数重载,编译器可以通过调用它们的对象来区分,常对象只能调用常函数,非常对象优先调用非常函数,若不存在则调用常函数
3.常函数只能放在类内
c++ 复制代码
#include <iostream>


using namespace std;
class Person{
    string name;
    mutable int age;//表示该变量在常函数中可修改
public:
    Person(string name,int age):name(name),age(age){}
    void set_name(){
        this->name="海峰";
        cout<<"name non_const"<<endl;
    }
    void set_name() const{
//        this->name="海峰";
        cout<<"name const"<<endl;
        //常函数中不能调用非常函数,因为this指针不匹配
        //只能调用常函数
    }
    void set_age(){
        this->age=21;
        cout<<"age non_const"<<endl;
    }
    void set_age() const{
        this->age=21;
        cout<<"age const"<<endl;
    }
};
//void func() const{
//
//}//常函数只能定义在类内
int main(){
    Person p("施易辰",20);
    p.set_name();
    const Person const_p("李昊",20);
    const_p.set_age();
    return 0;
}

10.static关键字

首先说一下static修饰局部变量

只能被初始化一次,生命周期会被延长。
一般使用场景是解决不能返回局部变量的引用,但是可以通过static关键字延长声明周期解此问题
c++ 复制代码
#include <iostream>


using namespace std;
void func(){
    for(int i=0;i<5;i++){
        //静态局部变量只能被初始化一次
        static int a=0;
        cout<<++a<<endl;
    }
}
int& func2(){
    int a=1;
    //因为非静态局部变量的声明周期和作用域都是{begin}end
    //解决方案
    static int b=1;
    return b;
}
int main(){
    cout<<func2()<<endl;
    return 0;
}

然后是static修饰全局变量

它的作用域会被缩短,到当前文件
使用场景一般为:如果向在头文件中定义全局变量,那么需要定义为静态全局变量,否则会引发重定义问题

然后是static修饰成员变量

1.静态成员变量存储在静态区,不占用对象空间,分配内存在编译阶段,主函数之前
2.静态成员变量必须类内声明类外初始化,因为静态成员变量属于类不属于对象,如果你在构造函数中初始化,岂不是每一个对象都有一个静态成员变量了
c++ 复制代码
#include <iostream>


using namespace std;

class A{

public:
 static int a;//类内声明
//    A(int _a){
//        a=_a;
//    }
};
int A::a=1;//类外初始化

int main(){
 A b;
 return 0;
}
3.调用:可以通过类名或者对象名调用
4.静态成员变量在继承关系中子类和父类共享

然后是static修饰成员函数

1.静态成员函数没有this指针,无法调用非静态成员
2.调用:可以通过类名或者对象调用

11.函数重写与函数隐藏

首先说一下函数重写

函数重写指的是在继承关系中,子类重写了基类的虚函数,并且该虚函数具有和子类重写函数相同的返回值、名字和参数

然后是函数隐藏

函数隐藏也是方式在继承关系中,如果函数名相同,不是重写就是隐藏
c++ 复制代码
#include <iostream>


using namespace std;

class Base{
public:
    void func(){
        cout<<"Base:: func"<<endl;
    }
    virtual void vFunc() {
        cout<<"Base vFunc"<<endl;
    }
};
class Child:public Base{
public:
    void func(){
        cout<<"Child:: func"<<endl;
    }
    void vFunc()override{
        cout<<"Child vFunc"<<endl;
    }
    void vFunc(int a){
        cout<<a<<endl;
    }
};
class son:public Child{
    void vFunc()override{
        cout<<"son vFunc"<<endl;
    }
};
int main(){
    Child c;
    c.Base::func();
    return 0;
}

最后是函数的重载、重写和隐藏之间的区别

重载:发生在同一类之间,函数名相同但参数列表不同
重写:发生在继承关系中,父子类之间,子类重写了父类的虚函数,并且父类的虚函数和子类的重写函数之间函数名、返回值和参数都相同
函数隐藏:发生在继承关系同,父子类之间,如果函数名相同,不是重写就是隐藏(子类继承了父类的函数,但子类中又由一个自己实现的同名函数,此时子类会隐藏父类的同名函数,要想访问父类的同名函数要加作用域区别)

12.多态

多态分为静态多态和动态多态。
静态多态是指编译阶段的多态,在编译时就可以绑定函数的地址,确定执行哪个函数
动态多态是值运行阶段的多态,在运行阶段才能绑定号函数的地址,确定执行哪个函数
静态多态包括:函数重载、运算符的重载和模板
动态多态包括:父类指针指向子类对象,然后父类的指针或者引用去调用重写的虚函数

动态多态的调用过程

首先父类指针或引用会在找到子类对象中的虚表指针,然后通过虚表指针找到虚表,在虚表中找到虚函数的地址,然后调用该虚函数

虚函数表

虚表的本质是一个指针数组,里面存储本类中虚函数的地址,在编译阶段被创建,每个类只有一个虚表,所有对象共享一个虚表。

虚表指针

每个对象都有一个虚表指针,虚表指针指向虚表,它在构造函数中被赋值

13.纯虚函数与抽象类

什么是纯虚函数?

纯虚函数是虚函数的基础上函数体部分被赋值为0

纯虚函数虚表中存储的是0

含有纯虚函数的类叫抽象类

抽象类一般作为基类
抽象类不能创建对象
如果子类不重写父类的纯虚函数,则子类仍为抽象类

14.内联函数

内联展开

内联函数会在编译阶段会进行内联展开,在函数调用处直接展开函数实现,所以不用进行函数调用,避免了开辟栈帧等操作带来的开销,提高了代码的执行效率。

什么样的函数适合设置为内联函数?

函数体很少,函数调用的时间远远大于函数执行的时间,那么这个函数就可以设置为内联函数,内联函数在编译阶段进行内联展开,在函数调用处直接展开函数实现,避免函数的调用

编译器说了算

用户虽然定义了该函数为内联函数,但在编译时这个函数是否是内联函数由编译器决定。用户相当于给编译器起了个建议

【C++】 内联函数详解(搞清内联的本质及用法)-CSDN博客

一个函数总花费的时间等于函数的调用时间(开辟栈帧等操作的时间)+该函数的执行时间,有些函数代码可能很少,开辟栈帧的时候远远大于函数执行的时间,那么这时候可以把这个函数设置为内联函数(inline),内联函数在编译阶段会把函数调用直接用函数体替换,避免了开辟栈帧等操作的时候。提高了代码的执行效率

15.堆和栈

栈区和堆区

在C++中一般用new来申请堆区空间,用delete去释放堆区空间,而C中一般用malloc来申请堆区空间,free释放。(new malloc区别 delete和free区别)
栈区不需要手动申请空间,又不用手动释放

数据结构中的栈和堆

栈是一种后进先出的数据结构
堆分为大根堆和小根堆,C++中的STL提供了优先级队列去实现大根堆和小根堆,默认的优先级队列是大根堆,要想变成小根堆可以重写比较规则
c++ 复制代码
#include <iostream>
#include <queue>

int main(){
 priority_queue<int,vector<int>,less<int>> bigPq;
 //堆中的元素是什么类型,用容器去实现堆,比较规则
 priority_queue<int,vector<int>,less<int>> smPq;
	return 0;
}
堆排序
C++提供的sort底层就是堆排序,时间复杂度是O(nlogn)

16.构造函数能声明成虚函数吗?

c++ 复制代码
#include <iostream>



using namespace std;

class A{
    
public:
    virtual A(){
        //构造函数不能声明成虚函数
        //首先调用虚函数需要通过调用虚表指针,然后找到虚表,虚表中
        //找到地址,然后调用虚函数
        //且虚表指针是在构造函数中赋值的,也就是说构造函数没执行之前是
        //没有虚表指针的,所以构造函数不能声明成虚函数
    }
};
int main(){
    
    
    return 0;
}

17.析构函数可以是虚函数

当有父类指针或引用指向子类对象的时候,父类析构函数必须设置为虚析构。因为父类指针或引用的调用范围是父类的非虚函数以及子类的重写函数。当delete的时候,该指针只能调用父类的非虚析构,无法调用子类的析构,会造成内存的泄漏。如果把父类的析构函数定义为虚析构,则子类的析构为重写的析构函数,指针就可以调用子类的析构了,并且子类析构里面自动调用父类的析构函数。
c++ 复制代码
#include<iostream>

using namespace std;
class base{
public:
    base(){
        cout<<"base构造"<<endl;
    }
    virtual ~base(){
        cout<<"base析构"<<endl;
    }
};
class son:public base{
public:
    son(){
        cout<<"son构造"<<endl;
    }
    //在继承过程中,编译器会把子类和父类的析构函数统一看成destructor同名析构函数
    //所以会发生函数隐藏
    ~son(){
        cout<<"son析构"<<endl;
        //base::~base();
    }
};
int main(){
    base *b=new son;//父类指针指向子类对象,调用父类的非虚函数和子类重写的虚函数
    //发生函数隐藏,只会调用base析构
    delete b;
    return 0;
}

18.什么是内存泄漏?

Memory Leak
比如在堆区new出来的空间,没有delete,操作系统无法再将这段空间分配给其他进程,就会造成内存的泄漏。一次泄漏没什么关系,但是多次的泄漏会造成内存的溢出。

19.如何只创建堆区对象?

栈区无法申请对象,析构函数私有化
c++ 复制代码
#include <iostream>


using namespace std;
class MyClass{
    ~MyClass(){
        cout<<"MyClass::Destructor"<<endl;
    }
public:
    MyClass(){
        cout<<"MyClass::Constructor"<<endl;
    }
};

int main(){
    //栈区对象是由编译器来管理的,在编译阶段创建堆区对象时会检查该对象的析构函数是否能够被调用
    //如果析构函数无法被调用则编译器不会创建栈区对象
    MyClass myClass;

    return 0;
}

20.如何只创建栈区对象?

new运算符重载,调用alloca函数,该函数可以在栈区申请空间并返回这段空间的首地址
c++ 复制代码
#include <iostream>


using namespace std;
class MyClass{
    ~MyClass(){
        cout<<"MyClass::Destructor"<<endl;
    }
public:
    MyClass(){
        cout<<"MyClass::Constructor"<<endl;
    }
};
void* operator new(size_t size){
    void *ptr = alloca(size);//alloca可以在栈区上申请内存
    cout<<"栈区分配"<<endl;
    return ptr;
}
int main(){
    MyClass *myClass = new MyClass();
    return 0;
}

21.什么情况下发生内存泄漏?

1.函数内用new申请的空间在函数结束之后没有被释放
2.父类指针指向子类对象,父类的析构函数不是虚析构
c++ 复制代码
//编译器在编译的时候会把父子类的析构函数认为成同名的构造函数,会发生函数隐藏,delete的时候调用父类的析构函数,造成内存的泄漏,如果父类时虚析构,发生函数重写,就可以通过指针调用子类的析构函数,子类中会默认调用父类的析构函数,不会造成内存的泄漏
3.指针的重写复制,导致原来指向的空间无法在被找到了,OS无法将这块内存重写分配
4.智能指针的循环引用问题
c++ 复制代码
#include <iostream>
#include <memory>

using namespace std;
class B;

class A{
public:
 shared_ptr<B> ptr;
 ~A(){
     cout<<"A::destructor"<<endl;
 }
};

class B{
public:
 shared_ptr<A> ptr;
 ~B(){
     cout<<"B::destructor"<<endl;
 }
};

int main(){
 shared_ptr<A> pa(new A());
 shared_ptr<B> pb(new B());
 pa->ptr=pb;
 pb->ptr=pa;
 return 0;
}

22.构造函数前可以执行哪些函数

静态成员函数和全局函数

23.类的大小由什么来决定?

空类的大小为1字节

为了创建对象方便

非静态成员变量的大小

因为静态成员变量存储在静态区,属于类不属于对象,不占对象的空间

是否由虚函数

因为有虚函数就要创建虚表,有虚表就需要有虚表指针,虚表指针属于对象,每个对象都有一个虚表指针

是否有虚继承

首先虚继承是为了解决菱形继承产生的二义性问题
c++ 复制代码
#include <iostream>


using namespace std;
class D{
public:
 int D_a;
};
class B:virtual public D{
public:
 int B_val;
};
class C:virtual public D{
public:
 int C_val;
};
class A:public B,public C{

};

int main(){
 A a;
 //多继承和菱形继承都会产生二义性问题解决方法是作用域,菱形继承还有另外一个解决方法就是虚继承
 a.B_val;
 a.C_val;
 a.D_a;
 return 0;
}

内存的对齐方式

首先为什么又内存对齐是因为内存的物理结构的原因
对齐规则:按最大字节对齐,遵循整数倍原则(索引从0开始)

24.虚函数与纯虚函数的区别?

从函数构成上来看

虚函数有函数体,纯虚函数没有函数体

从类上来看

含有纯虚函数的类叫抽象类,不能创建对象

从子类是否需要重写上来看

虚函数子类可以选择性重写(也就是可以不重写)
但纯虚函数如果子类想创建对象的话,必须重写该父类的纯虚函数,否则子类仍为抽象类

从虚表上来看

虚函数在虚表中存放的是虚函数的地址,纯虚函数在虚表中存放的是0

25.既然又了malloc和free为什么还要又new和delete?

因为c++中引入了类和对象的概念,类中又构造函数和析构函数
new是在堆区中申请一块空间后返回该空间的首地址,然后再调用构造函数给成员变量赋值,delete是先调用析构函数释放成员变量指向的堆区空间,然后再调用free函数释放对象。
至于为什么delete是先调用析构再调用free?
如果先调用free对象释放了,成员变量所指向的堆区空间就无法找到了,因为该成员变量已经被释放了,会造成内存溢出
而malloc只是申请空间,不能给对象赋值。free只能释放对象所在的空间,无法释放对象中成员变量所指向的堆区空间

26.结构体和类的区别?

默认访问权限和默认继承权限不同
结构体的默认访问和默认继承权限都是public,类的默认访问和默认继承权限都是private

27.被delete和free释放的空间会直接返回给操作系统吗?

不会,会被存放到内存池中,ptmalloc会维护一个空闲内存双向链表,下次用户再申请的时候先再内存池中寻找,解决了内存碎片化问题。

一篇文章彻底讲懂malloc的实现(ptmalloc)_malloc过程-CSDN博客

用户 free 掉的内存并不是都会马上归还给系统,ptmalloc 会统一管理 heap 和 mmap 映射区域中的空闲的 chunk,当用户进行下一次分配请求时,ptmalloc 会首先试图在空闲的chunk 中挑选一块给用户,这样就避免了频繁的系统调用,降低了内存分配的开销。

ptmalloc将相似大小的 chunk 用双向链表链接起来,这样的一个链表被称为一个 bin。Ptmalloc 一共维护了 128 个 bin,并使用一个数组来存储这些 bin(如下图所示)。

   如果每次free掉的内存都还给OS的话,尤其是在小字节的情况下,那么造成的情况,就是一大块的内存被你弄的千疮百孔,也就是说一块内存,里面有很多gap。

在操作系统的虚拟内存管理中,管理着的都是固定大小的内存,如4K,那你还给我1 Byte或5 Byte, 那么OS显然是很尴尬的。

  于是为了避免这样的问题,那么内存管理一般会有一个free block list,free掉的东西就放在这里来。那么你可能会释放很散乱的内存过来,没关系,我们在这里会尝试合并这些散乱的block,而malloc首先找的也是free block list,而非从OS申请新的内存。

那么此时如果找到了一块儿合适的自然最好,如果找到的是比要的更大,那么一部分malloc,另一部分放回去。

当然,由于malloc和free是如此普遍,自然会尝试着让它变的更好,所以也有各种优化,如对上图中free block list进行chunk size排序,什么情况下合并,什么时候分割,内存的分配算法等,都有很多机制来管理小内存。

28.什么时候需要析构函数?

1.类中又指针类型的成员变量的时候
2.当父类指针或引用指向子类对象的时候,父类必须是虚析构

29.常函数能否修改静态成员?

常函数的this指针类型位常指常类型,所以无法修改非静态成员变量,也无法访问非静态成员函数,因为this指针不兼容。
但常函数中可以访问静态成员,因为静态成员属于类不属于对象,不用this指针去访问。

30.初始化参数列表有什么特点?

初始化列表顾名思义就是用来给成员变量初始化的,只能在构造函数中使用,有些变量不初始化也可以,在构造函数中赋值就可以了,但是有些成员变量必须初始化,比如父类继承过来的变量,必须在列表中显示调用进行初始化,类成员必须括号调用进行初始化,还有引用变量也必须在列表中初始化
c++ 复制代码
#pragma once
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
class A{
    int a1;
    int a2;
public:
    A(int _a1,int _a2){
        a1=_a1;
        a2=_a2;
    }
};
class B:public A{
    int b1;
    A a3;
public:
    B(int _a1,int _a2,int _b1,int _a31,int _a32):
    //初始化列表-->1.给继承过来的父类的成员变量初始化,必须通过父类名显示调用
    //            2.给本类下的成员变量进行初始化,类成员通过对象名括号调用构造函数
    A(_a1,_a2),a3(_a31,_a32){
        b1=_b1;
    }
};
void text2_01(){
    B b(1,2,3,4,5);
    //调用无参构造函数不用加括号
} 

31.C++生成可执行文件的四个步骤

预处理

该阶段只要进行头文件的展开,宏替换等操作,生成的文件仍然是c语言文件

编译

该阶段主要进行词法语法的分析,生成的文件是汇编语言文件

汇编

该阶段是将汇编语言文件翻译成机械语言文件,生成的是可重定位的机械语言文件

链接

该阶段将各个目标文件进行链接,生成可执行文件。

32.什么是悬挂指针?

听名字就很危险,所以是野指针,指向了一段未知的、不可控的区域

33.什么时候会出现悬挂指针?

指针变量为初始化
new 出来的空间释放掉了,指针为置空
数组访问越界
realloc函数使用不当

realloc函数的功能是把原来空间的内容赋值,然后重新申请空间把内容拷贝过来

如果申请的空间过于大了,无法在原来空间的基础上扩容,导致原来空间的指针变成了野指针

c++ 复制代码
#include <iostream>
#include <cstdlib>
using namespace std;
class A{
int a,b;
public:
A(int _a,int _b):a(_a),b(_b){

}
};

int main(){
int *ptr = (int*)malloc(sizeof(int));
*ptr=2;
int *newptr = (int*)realloc(ptr,10000*sizeof(int));
cout<<ptr<<' '<<newptr<<endl;
return 0;
}

34.#define和const有什么区别

define 在预处理阶段替换,不会进行语法词法的检查
const在编译阶段替换,会进行语法词法的检查

35.include < >和include " "的区别

<>通常用于包含标准头文件。编译器会在系统指定的标准头文件所在的目录中查找
""通常用于包含用户自定义的头文件。编译器会先在当前项目所在的文件所在目录查找,如果没有找到,再到系统指定的标准头文件所在目录查找

36.C++声明和定义的区别

声明

声明是告知编译器这个变量或者是函数的存在,不回为变量分配实际空间也不会为函数详细定义它的行为。

定义

不仅声明了变量和函数的存在,而且为起实际分配了空间定义了其详细的行为。

可以重复声明,但是不可以重复定义

37.引用作为返回值的好处和应该遵循的规则

引用作为返回值可以避免调用拷贝构造,拷贝匿名对象,减少内存开销。直接返回对象本身,提高效率

引用作为返回值可以支持链式操作,比如cin>>a>>b>>c;cout<<a<<b<<c

c++ 复制代码
#include <iostream>

using namespace std;
class Box {
	int length;
	int width;
	int high;
	friend ostream& operator<<(ostream& o, const Box& b);
	friend istream& operator>>(istream& i, Box& b);
public:
	Box() {
		length = 1;
		width = 2;
		high = 3;
	}
};
ostream& operator<<(ostream& o,const Box& b) {
	o << b.length << ' ' << b.width << ' ' << b.high << endl;
	return o;
}
istream& operator>>(istream& i,Box& b) {
	i >> b.length;
	i >> b.width;
	i >> b.high;
	return i;
}
int main() {
	Box a, b,c;
	//cout << a;没有与这些操作数相匹配的运算符
	/*
	* 你想重载一个运算符要么在类内重载,要么在类外重载
	* 但是cout对象属于ostream类,该类我们无法修改,所以只能在类外用全局函数重载
	*/
    cin >> a>>b>>c;
    cout << a << b << c;
	return 0;
}

不要返回局部变量的引用

重载±*/运算符的时候不能引用返回,因为它们在重载运算符函数里面都引入了局部变量,并且想要返回这个局部变量,所以必须值返回
重载前++运算符,不用引用返回
重载后++运算符,由于要引入局部变量,并且返回该局部变量所以要值返回

38.成员函数通过this指针来区分不同的对象

在函数调用的时候this指针作为隐含参数传到了成员函数中,this存放的是调用该成员函数对象的地址,成员函数可以通过this指针来区分究竟是堆哪个对象进行操作。
就比如现在有一个Person类里面有一个属性age,当不同的Person类的对象调用成员函数的是,成员函数可以通过this指针来区分究竟访问哪个对象的age

39.C++编译器为类提供的五个缺省函数

默认构造函数

默认析构函数

默认拷贝构造函数

默认移动构造函数

默认运算符重载函数

40.在类外有什么办法访问非公有成员

通过友元

可以将全局函数或者是类声明为要访问私有成员类的友元类,这样就可以访问该类的私有成员了

41.在继承关系中,在栈区创建子类对象构造和析构的调用顺序

构造顺序

先调用父类的构造,再调用子类的构造

析构顺序

先调用子类的析构,再调用父类的析构

42.什么是空白基类优化?

首先说一下什么是空白类

空白类就是指用户创建类的时候没有声明和定义任何的属性和行为,那么这个类的大小默认是1字节。
因为对象是一个有类型的内存,为了方便申请空间,给空白类默认大小是1字节

然后说一下什么是空白基类优化

当空白类作为基类的时候,子类继承的父类大小会被优化为0字节,这种优化被称为空白基类优化

43.虚函数可以成为内联函数吗?

首先内联函数在编译阶段,在函数调用处直接展开函数体,减少开辟栈帧等操作带来的开销。
虚函数是靠虚表实现的一种动态绑定机制,在运行阶段才能最终确定绑定函数的地址。
如果是对象调用虚函数,编译器在编译阶段可以确定调用哪个虚函数,因此可以将虚函数内联展开
如果是指针或者引用调用虚函数,则在编译阶段无法确定调用哪个函数,因此无法将虚函数内联展开

44.抽象类为什么不能创建对象?

首先含有抽象方法的类叫抽闲类,抽象方法是一个虚函数声明,没有具体的实现。
抽象类的只要作用是作为基类(接口类),定义接口,去让子类去实现这些接口
就假设你真的创建出来抽象类的对象了,该对象去调用抽象类里面的抽象方法,但是抽象方法没有函数的实现,编译器会报错,没有意义啊。

45.成员函数中delet this之后对象还可以使用吗?

c++ 复制代码
#include <iostream>


using namespace std;
class A{
    int a,b,c;
public:
    A(int _a,int _b,int _c):a(_a),b(_b),c(_c){}
    void Func(){
        a=1;
        delete this;
        b=2;
    }
};
int main(){
    A s(1,2,3);
    s.Func();
    return 0;
}

无法继续使用了

如果想在成员函数中调用delete this 必须保证delete之后你不会再去用this指针访问任何成员。

46.静态成员函数可以成为虚函数吗?

不可以,静态成员函数没有this指针,而虚函数的实现需要虚表,虚表需要虚表指针,而虚表指针再对象中,需要this指针调用,所以静态成员函数无法成为虚函数

47.如何阻止类创建对象?

定义抽象类

里面写抽象方法

把构造函数定义为私有,用静态成员函数去调用构造函数

c++ 复制代码
#include <iostream>


using namespace std;
class A{
    int a,b,c;
    A(int _a,int _b,int _c):a(_a),b(_b),c(_c){}
public:
    static void Func(int _a,int _b,int _c){
        A(_a,_b,_c);
    }
};
int main(){
    A::Func(1,2,3);
    return 0;
}

如何让类只能创建一个对象,实现单例模式

c++ 复制代码
#include <iostream>


using namespace std;
class Person{
public:
    //用静态成员方法getInstance创建对象
    static Person& getInstance(int age,string name){
        //static对象可以实现只创建一个对象,因为静态局部变量只能被初始化一次
        //另外一个好处是static修饰的变量在C++11之后是线程安全的,不用考虑线程同步问题
        static Person instance(age,name);
        return instance;
    }
private:
    int m_age;
    string m_name;
    //为了实现单例模式私有化其构造函数和拷贝构造函数
    Person(const Person& other) = delete;
    Person(int age,string name){
        m_age=age;
        m_name=name;
    }
};

int main(){
    Person &p = Person::getInstance(15,"施易辰");
    return 0;
}

48.C++内存分配

栈区

内存的开辟和内存的释放由操作系统自动进行,不需要用户主动申请或释放

堆区

内存的开辟和内存的释放需要用户手动进行

常量区

主要存放字符串常量,程序结束后由操作系统释放

全局区

全局变量、静态变量,不需要手动释放

代码区

存放函数的二进制代码

49.声明和定义的区别

声明

声明是告诉编译器这个变量或者函数的存在,不会和变量分配空间,也不会有函数的具体实现

定义

定义是会为变量分配具体空间,为函数添加具体的实现

50.x=x+1、x+=1、x++ 哪个效率高?

x++会慢一些,还记得重载后++运算符的时候怎么实现的吗

c++ 复制代码
#include <iostream>


using namespace std;
class A{

public:
    int a;
    A(int _a){
        a=_a;
    }
    A operator++(int){
        A t(*this);
        a++;
        return t;
    }
};

int main(){
    A obj(1);
    cout<<(obj++).a;
    return 0;
}

x++会创建一个临时变量来保存之前的值,所以它会比其他俩者效率慢一些

51.全局变量定义在头文件中会有哪些问题?

如果全局变量被定义在头文件中,并且给头文件还被多个.cpp文件包含的话,则该头文件中的全局变量会产生宠爱当以问题,解决方法是该为静态全局变量,加上static
c++ 复制代码
//main.cpp
#include <iostream>
#include "variable.h"

using namespace std;

int main(){
    return 0;
}
c++ 复制代码
//a.cpp
#include "variable.h"
c++ 复制代码
//variable.h
//int globalVar;//wrong
static int globalVar;

52.编译器会为类生成哪些函数?

默认构造函数
默认析构函数
默认拷贝构造函数
默认赋值运算符重载

53.C++构造函数有几种,分别有什么作用?

默认构造函数

在用户没有提供任何构造函数的适合,对类的成员变量进行默认赋值

有参构造函数

对类的成员变量进行灵活赋值,以满足各种具体的场景需求

拷贝构造函数

用一个已经存在的对象去初始化一个新的对象,把自己的资源赋值一份给另外一个对象

移动构造函数

C++11引入,配合move函数,可以处理右值引用,把自己的资源给别人,自己没有资源了,而不是把自己的资源赋值一份给别人,提高了资源的移动效率,特别是在设计临时对象的时候
c++ 复制代码
#include <iostream>

using namespace std;
class MyObject{
    int* data;
public:
    MyObject(): data(nullptr){
        cout<<"Default Constructor"<<endl;
    }
    MyObject(int value): data(new int(value)){
        cout<<"Regular Constructor"<<endl;
    }
    MyObject(MyObject&& other):data(other.data){
        other.data = nullptr;
        cout<<"Move constructor"<<endl;
    }
    ~MyObject(){
        delete data;
        cout<<"Destructor"<<endl;
    }
    void printData() const {
        if(data != nullptr) cout<<"Data: "<<*data<<endl;
        else                cout<<"Data is null"<<endl;
    }
};

int main(){
    MyObject obj1(10);
    obj1.printData();
    MyObject obj2(std::move(obj1));
    obj2.printData();
    obj1.printData();
    return 0;
}

54.C++类对象的初始化顺序?

先是基类的初始化

如果有基类先调用基类的构造函数,有多个基类的时候,把基类在子类继承表中的顺序进行

然后是类成员的初始化

如果类中包含别的类的成员,那么先基类初始化完之后,会对类成员进行初始化,初始化的顺序与类成员在该类中的声明顺序有关

最后才是自身构造函数的初始化

55.sizeof 和strlen的区别

sizeof是运算符而strlen的库函数

strlen接收字符指针类型的参数,计算字符串大小

sizeof计算各种类型的大小

c++ 复制代码
#include <iostream>
#include <cstring>

using namespace std;
char trg[]="ABCD";

int main(){
    cout<<sizeof(trg)<<endl;
    cout<<strlen(trg)<<endl;
    return 0;
}

56.谈谈你对拷贝构造函数和赋值运算符的认识

拷贝构造函数的调用时机是用一个已经存在的对象去初始化一个新的对象。
函数传参的时候如果是值传递会调用拷贝构造,函数返回的时候如果是值返回也会拷贝一个匿名对象,右值
拷贝构造分为深拷贝和浅拷贝,系统默认提供的拷贝构造函数就是浅拷贝,如果类中有成员变量在堆区中开辟空间的话,浅拷贝就会产生析构多次的问题,具体是因为浅拷贝只是一个值传递的过程,对于新对象而言,只会把堆区那段空间的地址赋值给新对象,不会重新开辟空间,这就导致了一块堆区空间被俩个对象指向,在对象销毁的时候会调用析构函数,该空间会被析构多次,会报错。解决的方法就是深拷贝,赋值的时候重新开辟一块空间,然后把空间的数据拷贝一份复制过来,这个俩个对象指向不同的堆区空间,但是空间里面的数据是一摸一样的。
一般对于自定义类来说系统会提供默认的赋值运算符,但是如果对象成员变量在堆区有资源的话,必须重写该赋值运算符,因为默认提供的不会给新的对象重新分配空间而是让新对象也指向这块资源,会产生问题,可以向上面所所说的那让去我们重写operator=()函数实现深拷贝类型的赋值

57.vector中的push_back()和emplace_back()区别、以及使用场景

底层原理不一样

push_back()是接收一个对象作为参数,如果不是对象的话会先调用构造函数创建出这个对象,然后看是走拷贝构造还是移动构造给它放到容器尾部
C++11引入了emplace_back()是接收一个构造函数的参数,直接在容器的尾部把这个对象创建出来

效率

通常来讲emplace_back的效率比push_back要快,因为emplace不用调用拷贝或者移动,直接在容器尾部创建对象。

适合场景

如果要添加的元素已经存在,并且需要进行复制或者移动操作,那么push_back比较合适。
如果直接要在尾部构造新的对象,并且希望避免复制或者移动的话,那么emplace_back()更合适

58.map和unordered_map的区别以及使用场景

map的底层是红黑树,是一种有序的数据结构,可在logn时间复杂度下实现查找。
unordered_map底层是哈希表,是一种无序的数据结构,可在O(1)时间复杂度下查找
对于不需要关心数据存储顺序,要求查找效率时,使用unordered_map,反之则用map

59.new申请的内存可以使用free释放吗?

如果成员变量没有在堆区申请空间的话,可以使用free释放,因为delete和free最主要的区别就时会调用析构函数,而析构函数的作用是释放成员变量所指向的堆区空间,如果没在堆区申请空间的话,析构函数也就没有作用,delete和free没区别。但是如果申请空间了,那么用free会造成内存的泄漏

60.纯虚函数的使用场景又哪些?

实现接口类(抽象类)

含有纯虚函数的类叫抽象类,它一般作为基类,不能创建对象只要的目的是为了作用基类让子类去重写抽象类里面的抽象方法

实现接口规范

支持多态

因为满足多态的条件,在继承关系中,子类重写了父类的虚函数,在函数调用的适合会触发动态多态。
比如我的QT网盘项目中,自定义了一个MyTcpServer类去继承QTcpServer,QTcpServer类中又一个虚函数叫incomingConnection,是一个槽函数,我自定义了一个MyTcpServer去继承该类,就可以重写该虚函数

设计框架和库

61.多态的意义

多态的形式是不同的对象通过调用同一个函数实现不同的行为。具体而言是,不同的子类重写了父类的虚函数,通过父类指针指向子类对象的这种形式,通过虚表来实现多态。

62.实现String类

c++ 复制代码
------------------------------------------MyString.h---------------------------------------------------------------
//
// Created by 17440 on 2024/6/17.
//

#ifndef TEST01_MYSTRING_H
#define TEST01_MYSTRING_H
#include <iostream>
using namespace std;

class MyString {
    char* data;
    int size;
public:
    MyString();
    MyString(int n, char c);
    MyString(const char* source);
    MyString(const MyString& other);
    friend ostream& operator<<(ostream& o,MyString s);
    char operator[](int i);
    char operator[](int i)const;
    bool operator==(const MyString& other);
    ~MyString();
};


#endif //TEST01_MYSTRING_H
c++ 复制代码
-----------------------------------------------MyString.cpp--------------------------------------------------------
    //
// Created by 17440 on 2024/6/17.
//

#include <cstring>
#include "MyString.h"

MyString::MyString() {
    size = 0;
    data = new char[1];
    *data = '\0';
}

MyString::MyString(int n, char c) {
    size = n;
    data = new char[n+1];
    char *ptr = data;
    while(n--){
        *ptr++ = '\0';
    }
    *ptr = '\0';
}

MyString::MyString(const char *source) {
    if(source == nullptr){
        size = 1;
        data = new char[1];
        *data = '\0';
    }
    else{
        size = strlen(source);
        data = new char[size+1];
        memcpy(data,source,size);
        *(data + size) = '\0';
    }
}
MyString::MyString(const MyString& other) {
    this->size = other.size;
    data = new char[size+1];
    memcpy(this->data,other.data,size + 1);
}

MyString::~MyString() {
    cout<<"析构函数"<<endl;
    if(data != nullptr){
        delete[] data;
        data = nullptr;
        size = 0;
    }
}

char MyString::operator[](int i){
    return this->data[i];
}

char MyString::operator[](int i) const {
    return this->data[i];
}

bool MyString::operator==(const MyString &other) {
    if(this->size != other.size) return false;
    char* p = this->data;
    char* q = other.data;
    while(*p && *q){
        if(*(p++) != *(q++)) return false;
    }
    return true;
}

ostream &operator<<(ostream &o, MyString s) {
    for(int i = 0; i < s.size){

    }
}

63.vector如何扩容?

vs1.5倍扩容,GCC二倍扩容,扩容是需要重写申请一块空间,然后把旧内存中的数据拷贝过去。

64.什么是迭代器失效?

相关推荐
hunandede21 分钟前
av_image_get_buffer_size 和 av_image_fill_arrays
c++
怀澈1222 小时前
高性能服务器模型之Reactor(单线程版本)
linux·服务器·网络·c++
chnming19872 小时前
STL关联式容器之set
开发语言·c++
威桑2 小时前
MinGW 与 MSVC 的区别与联系及相关特性分析
c++·mingw·msvc
熬夜学编程的小王3 小时前
【C++篇】深度解析 C++ List 容器:底层设计与实现揭秘
开发语言·数据结构·c++·stl·list
yigan_Eins3 小时前
【数论】莫比乌斯函数及其反演
c++·经验分享·算法
Mr.133 小时前
什么是 C++ 中的初始化列表?它的作用是什么?初始化列表和在构造函数体内赋值有什么区别?
开发语言·c++
阿史大杯茶3 小时前
AtCoder Beginner Contest 381(ABCDEF 题)视频讲解
数据结构·c++·算法
C++忠实粉丝3 小时前
计算机网络socket编程(3)_UDP网络编程实现简单聊天室
linux·网络·c++·网络协议·计算机网络·udp
我们的五年3 小时前
【Linux课程学习】:进程描述---PCB(Process Control Block)
linux·运维·c++