C++面经

本文详细介绍了C++编程中的基本概念和技术,包括内存管理(如构造函数、析构函数、拷贝构造函数、内存分配)、面向对象特性(如继承、多态、抽象类)、模板(如函数模板、类模板、模板特化)、STL容器(如map、vector、list)的操作以及C++11的新特性,如智能指针和移动语义。文章还探讨了编程实践中如何避免内存泄漏和提高效率的策略。
摘要由CSDN通过智能技术生成

1.基本语法

1.1 struct 和 class 区别

·struct的成员默认是公有的,而class的成员默认是私有的

·C中的struct不能包含成员函数,C++中的class可以包含成员函数

1.2 new 和malloc 的区别

·都可用来申请动态内存和释放内存,都是在堆(heap)上进行动态的内存操作

·malloc和free是c语言的标准库函数,new/delete是C++的运算符

·new会自动调用对象的构造函数,delete 会调用对象的析构函数, 而malloc/free返回的都是void指针

1.3 C++的内存分配

内存的5个分区:

堆区:一般由程序员自动分配,如果程序员没有释放,程序结束时可能有OS回收。其分配类似于链表。
栈区:由编译器自动分配和释放,存放为运行函数分配的局部变量,函数参数,返回数据,返回地址等,其操作类似于数据结构总的栈。
全局区(静态区static):存放全局变量,静态变量,常量。结束后由系统释放。
常量区(文字常量区):存放常量字符串,程序结束后有系统释放。
代码区:存放函数体(类成员函数和全局区)的二进制代码。

1.4 extern 和 static 的区别,什么情况用前者什么情况用后者

·extern---申明外部变量:它属于变量声明,extern int a和int a的区别就是,前者告诉编译器,有一个int类型的变量a定义在其他地方,如果有调用请去其他文件中查找定义。

·static---申明静态变量:简单说就是在函数等调用结束后,该变量也不会被释放,保存的值还保留。即它的生存期是永久的,直到程序运行结束,系统才会释放,无需手动释放。

1.5  strcpy和memcpy的区别

·strcpy和memcpy都是在C语言和C++语言中用于复制内存块的函数,但它们在使用和效率上有所不同。

·strcpy用于将一个以null结尾的字符串从源地址复制到目标地址。它会复制整个字符串,包括null终止符,直到遇到null为止。如果源字符串长度超过目标地址所分配的内存空间,则会导致内存越界和缓冲区溢出问题。

·memcpy用于将一段内存块从源地址复制到目标地址,可以复制任意长度的内存块,而不仅限于字符串。memcpy不会关心内存块中是否有null终止符,而只是按照给定的长度复制内存块。因此,使用memcpy时需要确保目标地址有足够的内存空间,否则也会导致缓冲区溢出问题。

·在效率方面,memcpy通常比strcpy更快,因为它不需要扫描整个字符串来查找null终止符。另外,memcpy也可以进行一些优化,例如使用字长操作来提高复制速度。但是,由于strcpy具有更简单的语法和更高的可读性,因此在处理字符串时,通常首选strcpy函数。

1.6  C++程序编译过程

1.预处理:处理以 # 开头的指令;

2.编译:将源码 .cpp 文件翻译成 .s 汇编代码;

3.汇编:将汇编代码 .s 翻译成机器指令 .o 文件;

4.链接:汇编程序生成的目标文件,即 .o 文件,并不会立即执行,因为可能会出现某些 cpp 文件中的函数引用了另一个 .cpp 文件中定义的符号或者调用了某个库文件中的函数。那链接的目的就是将这些文件对应的目标文件连接成一个整体,从而生成可执行的程序 .exe 文件。

1.7 全局变量定义在头文件会引发的问题

如果在头文件中定义全局变量,当该头文件被多个文件 #include 时,该头文件中的全局变量就会被定义多次,导致重复定义,因此不能在头文件中定义全局变量。

1.8 右值引用与移动语义

int i = 0;
int& j = i;//左值引用

int&& i = 0;//右值引用

右值引用和移动语义是C++11中新增加的一个很重要的特性,他主是要用来解决C++98/03中遇到的两个问题:

问题1.临时对象非必要的昂贵的拷贝操作

#include <iostream>
using namespace std;
 
class demo{
public:
   demo():num(new int(0)){
      cout<<"construct!"<<endl;
   }
   //拷贝构造函数
   demo(const demo &d):num(new int(*d.num)){
      cout<<"copy construct!"<<endl;
   }
   ~demo(){
      cout<<"class destruct!"<<endl;
   }
private:
   int *num;
};
 

int main(){
    demo a;
    demo b(a);
    return 0;
}

输出:
construct!
copy construct! //深拷贝
class destruct!
class destruct!

 解决办法如下:


#include <iostream>
using namespace std;
class demo{
public:
    demo():num(new int(0)){
        cout<<"construct!"<<endl;
    }
    demo(const demo &d):num(new int(*d.num)){
        cout<<"copy construct!"<<endl;
    }
    //添加移动构造函数
    demo(demo &&d):num(d.num){
        d.num = NULL;
        cout<<"move construct!"<<endl;
    }
    ~demo(){
        cout<<"class destruct!"<<endl;
    }
private:
    int *num;
};

int main(){
    demo a;
    demo b(move(a));//利用move()将左值变成右值
    return 0;
}
输出:
construct!
move construct! //移动语义--避免不必要的深拷贝
class destruct!
class destruct!

这个构造函数并没有做深拷贝,仅仅是将指针的所有者转移到了另外一个对象,同时,将参数对象a的指针置为空,这里仅仅是做了浅拷贝,因此,这个构造函数避免了临时变量的深拷贝问题。

问题2.c++98/03不能在模板函数中实现按照参数的实际类型进行转发,c++11实现该功能。

C++11引入了完美转发:在函数模板中,完全依照模板的参数的类型(即保持参数的左值、右值特征),将参数传递给函数模板中调用的另外一个函数。C++11中的std::forward正是做这个事情的,他会按照参数的实际类型进行转发。看下面的例子:

void processValue(int& a){ cout << "lvalue" << endl; }
void processValue(int&& a){ cout << "rvalue" << endl; }
template <typename T>
void forwardValue(T&& val)
{
    processValue(std::forward<T>(val)); //照参数本来的类型进行转发。
}
void Testdelcl()
{
    int i = 0;
    forwardValue(i); //传入左值 
    forwardValue(0);//传入右值 
}
输出:
lvaue 
rvalue

右值引用T&&是一个universal references,可以接受左值或者右值,正是这个特性让他适合作为一个参数的路由,然后再通过std::forward按照参数的实际类型去匹配对应的重载函数,最终实现完美转发。

1.9 什么是野指针和悬空指针

悬空指针:若指针指向一块内存空间,当这块内存空间被释放后,该指针依然指向这块内存空间,此时,称该指针为“悬空指针”。

void *p = malloc(size);
free(p); 
// 此时,p 指向的内存空间已释放, p 就是悬空指针。

野指针:指不确定其指向的指针,未初始化的指针为“野指针”。

void *p; 
// 此时 p 是“野指针”。

1.10 C++ 11 nullptr 比 NULL 优势

NULL:预处理变量,是一个宏,它的值是 0,定义在头文件 中,即 #define NULL 0

nullptr:C++ 11 中的关键字,是一种特殊类型的字面值,可以被转换成任意其他类型。

nullptr 的优势:

有类型,类型是 typdef decltype(nullptr) nullptr_t;,使用 nullptr 提高代码的健壮性。

函数重载:因为 NULL 本质上是 0,在函数调用过程中,若出现函数重载并且传递的实参是 NULL,可能会出现,不知和哪一个函数匹配的情况;但是传递实参 nullptr 就不会出现这种情况。

#include <iostream>
#include <cstring>
using namespace std;

void fun(char const *p)
{
    cout << "fun(char const *p)" << endl;
}

void fun(int tmp)
{
    cout << "fun(int tmp)" << endl;
}

int main()
{
    fun(nullptr); // fun(char const *p)
    /*
    fun(NULL); // error: call of overloaded 'fun(NULL)' is ambiguous
    */
    return 0;
}

1.11 指针和引用的区别

·指针所指向的内存空间在程序运行过程中可以改变,而引用所绑定的对象一旦绑定就不能改变。(是否可变)

·指针本身在内存中占有内存空间,引用相当于变量的别名,在内存中不占内存空间。(是否占内存)

·指针可以为空,但是引用必须绑定对象。(是否可为空)

·指针可以有多级,但是引用只能一级。(是否能为多级)

1.12 常量指针和指针常量的区别

·常量指针:

常量指针本质上是个指针,只不过这个指针指向的对象是常量。const 的位置在指针声明运算符 * 的左侧。只要 const 位于 * 的左侧,无论它在类型名的左边或右边,都表示指向常量的指针。(可以这样理解,* 左侧表示指针指向的对象,该对象为常量,那么该指针为常量指针。)

const int * p;//常量指针
int const * p;//常量指针

特点:指针指向的对象不能通过这个指针来修改,也就是说常量指针可以被赋值为变量的地址,之所以叫做常量指针,是限制了通过这个指针修改变量的值

·指针常量:

指针常量的本质上是个常量,只不过这个常量的值是一个指针。const 位于指针声明操作符右侧,表明该对象本身是一个常量,* 左侧表示该指针指向的类型,即以 * 为分界线,其左侧表示指针指向的类型,右侧表示指针本身的性质。

const int var;
int * const c_p = &var; //指针常量

特点:指针常量的值是指针,这个值因为是常量,所以指针本身不能改变,指针的内容可以改变

口诀简记const代表所指向的内容,* 代表指针,谁在左边表示谁不能动

1.13 函数指针和指针函数的区别

指针函数:本质是一个函数,只不过该函数的返回值是一个指针。相对于普通函数而言,只是返回值是指针

#include <iostream>
using namespace std;

struct Type
{
  int var1;
  int var2;
};

Type * fun(int tmp1, int tmp2){
    Type * t = new Type();
    t->var1 = tmp1;
    t->var2 = tmp2;
    return t;
}

int main()
{
    Type *p = fun(5, 6);
    return 0;
}

函数指针:本质是一个指针,只不过这个指针指向一个函数。函数指针即指向函数的指针。

#include <iostream>
using namespace std;
int fun1(int tmp1, int tmp2)
{
  return tmp1 * tmp2;
}
int fun2(int tmp1, int tmp2)
{
  return tmp1 / tmp2;
}

int main()
{
  int (*fun)(int x, int y); 
  fun = fun1;
  cout << fun(15, 5) << endl; 
  fun = fun2;
  cout << fun(15, 5) << endl; 
  return 0;
}
/*
运行结果:
75
3
*/

口诀简记看中文名右边两个字是什么本质就是什么

1.14 如何判断结构体是否相等?能否用 memcmp 函数判断结构体相等?

需要重载操作符 == 判断两个结构体是否相等,不能用函数 memcmp 来判断两个结构体是否相等,因为 memcmp 函数是逐个字节进行比较的,而结构体存在内存空间中保存时存在字节对齐,字节对齐时补的字节内容是随机的,会产生垃圾值,所以无法比较。

#include <iostream>

using namespace std;

struct A
{
    char c;
    int val;
    A(char c_tmp, int tmp) : c(c_tmp), val(tmp) {}

    friend bool operator==(const A &tmp1, const A &tmp2); //  友元运算符重载函数
};

bool operator==(const A &tmp1, const A &tmp2)
{
    return (tmp1.c == tmp2.c && tmp1.val == tmp2.val);
}

int main()
{
    A ex1('a', 90), ex2('b', 80);
    if (ex1 == ex2)
        cout << "ex1 == ex2" << endl;
    else
        cout << "ex1 != ex2" << endl; // 输出
    return 0;
}

1.15  include " " 和 <> 的区别

·查找文件的位置:#include<>是用于包含系统头文件的指令,通常会在编译器的标准库路径中搜索头文件。如果想要包含本地路径的头文件,应该使用#include"“指令,其中”“内的路径是相对于当前源文件所在的路径进行搜索的。因此,如果使用#include<>指令来包含本地路径的头文件,编译器可能会找不到该头文件,因为它只在标准库路径中搜索。相反,如果使用#include”"指令并提供正确的路径,编译器会在当前源文件所在的目录中搜索该头文件。

·使用习惯:对于标准库中的头文件常用 include<文件名>,对于自己定义的头文件,常用#include"文件名"

1.16 decltype关键字

decltype被称作类型说明符,它的作用是选择并返回操作数的数据类型

1.decltype + 变量:

const int ci = 0, &cj = ci;

// x的类型是const int
decltype(ci) x = 0;

// y的类型是const int &
decltype(cj) y = x;

2.decltype + 表达式(decltype会返回表达式结果对应的类型):

int i = 42, *p = &i, &r = i;

// r + 0是一个表达式
// 算术表达式返回右值
// b是一个int类型
decltype(r + 0) b;

// c是一个int &
decltype(*p) c = i;

3.decltype + 函数(C++中通过函数的返回值和形参列表,定义了一种名为函数类型的东西。它的作用主要是为了定义函数指针):

// 下面的函数就是上面的类型
int add_to(int &des, int ori);

//使用decltype获得函数add_to的类型
decltype(add_to) *pf = add_to;

1.17 隐式类型转换在哪些情况下会出现

1.对于内置类型,低精度的变量给高精度变量赋值会发生隐式类型转换,

2.对于只存在单个参数的构造函数对象构造来说,函数调用可以直接使用该参数传入,编译器会自动调用其构造函数生成临时对象。

2. 面向对象

2.1 什么是面向对象

就是一种对现实世界的理解和抽象,将问题转换成对象进行解决需求处理的思想。

2.2 构造函数和析构函数可不可以为虚函数

构造函数不可以是虚函数,如果构造函数是虚函数,那么就需要通过vtable(虚函数表参考链接) 来调用,但此时面对一块原始内存空间,到哪里去找 vtable呢?毕竟,vtable 是在构造函数中才初始化的啊,而不是在其之前。因此构造函数不能为虚函数。

析构函数可以为虚函数,因为当基类的指针引用指向派生类对象的时候,发生多态,如果不将基类的析构函数定义为虚函数的话,那么派生类的析构函数就无法执行。

2.3 拷贝构造函数如果用值传递会有什么影响

如果把拷贝构造函数的参数设置为值传递,那么参数肯定就是本类的一个object,采用值传递,在形参和实参相结合的时候,又要调用本类的拷贝构造函数(利用实参初始化局部变量(形参)),形成死循环。

2.4 如何限制一个类对象只能在堆(栈)上分配空间

在C++中,类的对象建立分为两种,一种是静态建立,如A a;另一种是动态建立,如A* ptr=new A;这两种方式是的区别:

1、静态建立类对象:是由编译器为对象在栈空间中分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数。

2、动态建立类对象,是使用operator new运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数。

2.4.1 只能在堆上分配空间

若要满足只能在上分配类对象,就是不能静态建立类对象,即不能直接调用类的构造函数,具体实现:

首先可能会想到将构造函数设为私有(实际不可行),在将构造函数私有之后,无法在类外部调用构造函数来构造类对象,只能使用new运算符来建立对象。然而,前面已经说过,new运算符的执行过程分为两步,C++提供new运算符的重载,其实是只允许重载new()函数,而new()函数只用于分配内存,后续构造对象仍然需要类的构造函数,此时构造函数私有而不可访问,故无法提供构造功能,因此,这种方法不可以。

改变思路,若当对象建立在栈上面时,是由编译器分配内存空间的,调用构造函数来构造栈对象。当对象使用完后,编译器会调用析构函数来释放栈对象所占的空间。编译器管理了对象的整个生命周期。如果编译器无法调用类的析构函数,情况会是怎样的呢?比如,将类的析构函数设置为私有的,编译器无法调用析构函数来释放内存。所以,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。因此,将析构函数设为私有,类对象就只能在堆上而不能在栈上开辟了。

拓展*:上述方式的缺点:

1.无法解决继承问题。如果A作为其它类的基类,则析构函数通常要设为virtual,然后在子类重写,以实现多态。因此析构函数不能设为private。还好C++提供了第三种访问控制,protected。将析构函数设为protected可以有效解决这个问题,类外无法访问protected成员,子类则可以访问。

2.类的使用很不方便,使用new建立对象,却使用destory函数释放对象,而不是使用delete。(使用delete会报错,因为delete对象的指针,会调用对象的析构函数,而析构函数类外不可访问)这种使用方式比较怪异。为了统一,可以将构造函数设为protected,然后提供一个public的static函数来完成构造,这样不使用new,而是使用一个函数来构造,使用一个函数来析构。

class A
{
protected:
    A(){}
    ~A(){}
public:
    static A* create()
    {
        return new A();
    }
    void destory()
    {
        delete this;
    }
};

2.4.2 只能在栈上分配空间

只有使用new运算符,对象才会建立在堆上,因此,只要禁用new运算符就可以实现类对象只能建立在栈上。虽然你不能影响new operator的能力(因为那是C++语言内建的),但是你可以利用一个事实:new operator 总是先调用 operator new,而后者我们是可以自行声明重写的。因此,将operator new()设为私有即可禁止对象被new在堆上


class A
{
private:
    void* operator new(size_t t){}     // 注意函数的第一个参数和返回值都是固定的
    void operator delete(void* ptr){} // 重载了new就需要重载delete
public:
    A(){}
    ~A(){}
};

原文链接:https://blog.csdn.net/baidu_16370559/article/details/123330995

2.5 public protected private 访问范围

private: 只能由该类中的函数、友元函数访问,不能被子类函数类的对象访问。
protected: 可以被该类中的函数、友元函数、子类的函数访问,但不能被类的对象访问。
public: 可以被该类中的函数、子类的函数、友元函数、类的对象访问。

注:友元函数包括两种:设为友元的全局函数,设为友元类中的成员函数

子类继承父类后,父类成员在子类中的访问属性:

使用private继承,父类的所有方法在子类中变为private;
使用protected继承,父类的protected和public方法在子类中变为protected,private方法不变;
使用public继承,父类中的方法属性不发生改变;

2.6 类都有哪几种构造方式

默认构造函数 Student();//没有参数

有参构造函数 Student(int num,int age);//有参数

拷贝构造函数 Student(Student&);//形参是本类对象的引用

转换构造函数 Student(int r) ;//形参时其他类型变量,且只有一个形参

2.7 拷贝构造函数参数中为什么有时候要加const

前面说过拷贝构造函数参数必须是引用类型,然后如果当拷贝构造参数为一个临时对象(右值)的时候,如果不加const,那么就是一个非常量左值引用(非常量左值是不能引用右值的),加了const之后就是一个常量左值引用,可以引用右值。

注:常量左值引用是一个“万能”的引用类型,可以接受左值,右值,常量左值、常量右值。需要注意的是普通的左值引用是不能接受右值的。

2.8 什么是多态

2.8.1 多态类型

1)动态多态

基类指针引用指向派生类对象时,编译时并不确定要执行的是基类还是派生类的虚函数;而当程序运行到该语句时,如果基类指针指向的是一个基类对象,则基类的虚函数被调用,如果基类指针指向的是一个派生类对象,则派生类的虚函数被调用。这种机制就叫作“(动态)多态(polymorphism)”。

2)静态多态(编译阶段,地址早绑定)

函数重载:包括普通函数的重载和成员函数的重载
函数模板的使用:通过将类型作为参数,传递给模板,可使编译器生成该类型的函数。

2.8.2 继承和(动态)多态区别

区别:继承是子类使用父类的方法,而多态则是父类使用子类的方法

2.8.3 虚函数可以内联吗

当呈现非多态的时候,虚函数可以内联。因为内联函数是在编译的时候确定函数的执行位置的。当函数呈现多态的时候,在编译的时候不知道是把基类的函数地址,还是派生类的函数地址写入虚函数表中,所以当非多态的时候就会将基类的虚函数地址直接写入虚函数表中,然后通过内联将代码地址写入。

2.9 虚函数与纯虚函数

虚函数:被 virtual 关键字修饰的成员函数,就是虚函数

纯虚函数:除了被 virtual 修饰,还需要在函数后面加上=0

        · 含有纯虚函数的类称为抽象基类(只要含有纯虚函数这个类就是抽象类),类中只有接口,没有具体的实现方法;

        · 继承纯虚函数的派生类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化对象。

#include <iostream>
using namespace std;

class A
{
public:
    virtual void v_fun() // 虚函数
    {
        cout << "A::v_fun()" << endl;
    }
    virtual void v_fun1() = 0; // 纯虚函数
};
class B : public A
{
public:
    void v_fun()
    {
        cout << "B::v_fun()" << endl; // 重写虚函数
    }
    void v_fun1()
    {
        cout << "B::v_fun1()" << endl; // 重写纯虚函数
    }
        
};
int main()
{
    A *p = new B();
    p->v_fun(); // B::v_fun()
    return 0;
}

2.10 单继承和多继承的虚函数表结构

如果有基类A和派生类B、C、他们的继承关系如代码所示:

class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
    void func1();
    void func2();
private:
    int m_data1, m_data2;
};
class B : public A {
public:
    virtual void vfunc1();
    void func1();
private:
    int m_data3;
};
class C: public B {
public:
    virtual void vfunc2();
    void func2();
private:
    int m_data1, m_data4;
};

则他们的虚函数表关系如下图所示:

 2.11 如何禁止类的构造函数的使用

为类的构造函数增加 = delete 修饰符,可以达到虽然声明了构造函数但禁止使用的目的。

#include <iostream>

using namespace std;

class A {
public:
    int var1, var2;
    A(){
        var1 = 10;
        var2 = 20;
    }
    A(int tmp1, int tmp2) = delete;
};

int main()
{
    A ex1;    
    A ex2(12,13); // error: use of deleted function 'A::A(int, int)'
    return 0;
}

2.12 什么是类的默认构造函数

默认构造函数:未提供任何实参,来控制默认初始化过程的构造函数称为默认构造函数。

2.13 如何避免拷贝(避免类对象、派生类、友元函数调用拷贝函数)

方法一

定义一个基类,将其中的拷贝构造函数和赋值构造函数声明为私有 private,且派生类以私有 private 的方式继承基类。

方法二

使用C++ 11 可以使用弃置函数delete关键字

class noncopyable {
protected:
    noncopyable() = default;
    ~noncopyable() = default;
public:
    noncopyable(const noncopyable&) = delete;
    noncopyable& operator=(const noncopyable&) = delete;
};

class foo : private noncopyable { 
};

2.14 如何减少构造函数开销

在构造函数中使用类初始化列表,会减少调用默认的构造函数产生的开销。

2.15 多重继承是什么?容易有什么问题?如何解决?

多重继承(多继承):是指从多个直接基类中产生派生类

多重继承容易出现的问题:命名冲突和数据冗余问题

解决办法:使用类作用域

不使用类作用域时出现命名冲突:

#include <iostream>
using namespace std;

// 间接基类
class Base1
{
public:
    int var1;
};

// 直接基类
class Base2 : public Base1
{
public:
    int var2;
};

// 直接基类
class Base3 : public Base1
{
public:
    int var3;
};

// 派生类
class Derive : public Base2, public Base3
{
public:
    void set_var1(int tmp) { var1 = tmp; } // error: reference to 'var1' is ambiguous. 命名冲突
    void set_var2(int tmp) { var2 = tmp; }
    void set_var3(int tmp) { var3 = tmp; }
    void set_var4(int tmp) { var4 = tmp; }

private:
    int var4;
};

int main()
{
    Derive d;
    return 0;
}

使用类作用域解决命名冲突:

#include <iostream>
using namespace std;

// 间接基类
class Base1
{
public:
    int var1;
};

// 直接基类
class Base2 : public Base1
{
public:
    int var2;
};

// 直接基类
class Base3 : public Base1
{
public:
    int var3;
};

// 派生类 
class Derive : public Base2, public Base3
{
public:
    void set_var1(int tmp) { Base2::var1 = tmp; } // 这里声明成员变量来源于类 Base2,当然也可以声明来源于类 Base3
    void set_var2(int tmp) { var2 = tmp; }
    void set_var3(int tmp) { var3 = tmp; }
    void set_var4(int tmp) { var4 = tmp; }

private:
    int var4;
};

int main()
{
    Derive d;
    return 0;
}

2.16  空类占多少字节

第一种情况(声明):

#include <iostream>
using namespace std;

class A
{
};

int main()
{
    cout << "sizeof(A):" << sizeof(A) << endl; // sizeof(A):1
    return 0;
}

对于空类,声明编译器不会生成任何的成员函数,只会生成 1 个字节的占位符。

第二种情况(定义对象):

#include <iostream>
using namespace std;
/*
class A
{}; 该空类的等价写法如下:
*/
class A
{
public:
    A(){};                                       // 缺省构造函数
    A(const A &tmp){};                           // 拷贝构造函数
    ~A(){};                                      // 析构函数
    A &operator=(const A &tmp){};                // 赋值运算符
    A *operator&() { return this; };             // 取址运算符
    const A *operator&() const { return this; }; // 取址运算符(const 版本)
};

int main()
{
    A *p = new A(); 
    cout << "sizeof(A):" << sizeof(A) << endl; // sizeof(A):1
    delete p;       
    return 0;
}

当空类 A 定义对象时,sizeof(A) 仍是为 1,但编译器会生成 6 个成员函数:缺省的构造函数、拷贝构造函数、析构函数、赋值运算符、两个取址运算符。

2.17  C++ 类对象的初始化顺序

#include <iostream>
using namespace std;

class A
{
public:
    A() { cout << "A()" << endl; }
    ~A() { cout << "~A()" << endl; }
};

class B
{
public:
    B() { cout << "B()" << endl; }
    ~B() { cout << "~B()" << endl; }
};
class C
{
public:
    C() { cout << "C()" << endl; }
    ~C() { cout << "~C()" << endl; }
};
class Test : public A, public B // 派生列表
{
public:
    Test() { cout << "Test()" << endl; }
    ~Test() { cout << "~Test()" << endl; }

private:
    B ex1;
    C ex2;
    A ex3;
    
};

int main()
{
    Test ex;
    return 0;

}
/*
A()
B()
B()
C()
A()
Test()
~Test()
~A()
~C()
~B()
~B()
~A()
*/

构造函数调用顺序:

·按照派生类继承基类的顺序,即派生列表中声明的顺序,依次调用基类的构造函数;

·按照派生类中成员变量的声名顺序,依次调用派生类中成员变量所属类的构造函数;

·执行派生类自身的构造函数

注:析构顺序和构造顺序相反

2.18 如何禁止一个类被实例化

方法一:在类中定义一个纯虚函数,使该类成为抽象基类,因为不能创建抽象基类的实例化对象;

方法二:将类的构造函数声明为私有 private

2.19 为什么用成员初始化列表会快一些

首先明确一点:数据类型可分为内置类型用户自定义类型(类类型),对于用户自定义类型,利用成员初始化列表效率高,内置类型则是一样的。

原因:用户自定义类型如果使用类初始化列表,直接调用该成员变量(自定义类型)对应的构造函数即完成初始化(仅仅调用一次构造函数);

如果在构造函数中初始化,因为 C++ 规定,对象的成员变量的初始化动作(调用构造函数)发生在进入构造函数本体之前,那么在执行构造函数的函数体之前首先调用成员的构造函数为其初始化,在进入函数体之后,又会调用一次该成员变量的构造函数赋初值。故使用初始化列表相较于在构造函数体内为自定义类型成员变量赋初值会少调用一次构造函数,效率会更高一点。

2.20  友元函数的作用及使用场景

友元提供了不同类的成员函数之间,或者类的成员函数与一般函数之间进行数据共享的机制。通过友元,一个不同函数或另一个类中的成员函数可以访问友元类中的私有成员保护成员

·类的成员函数与一般函数之间,通过友元使普通函数能够访问类的私有成员:

#include <iostream>

using namespace std;

class A
{
    friend ostream &operator<<(ostream &_cout, const A &tmp); // 声明为类的友元函数

public:
    A(int tmp) : var(tmp)
    {
    }

private:
    int var;
};

ostream &operator<<(ostream &_cout, const A &tmp)
{
    _cout << tmp.var;
    return _cout;
}

int main()
{
    A ex(4);
    cout << ex << endl; // 4
    return 0;
}

·不同类的成员函数之间共享数据:

#include <iostream>

using namespace std;

class A
{
    friend class B;

public:
    A() : var(10){}
    A(int tmp) : var(tmp) {}
    void fun()
    {
        cout << "fun():" << var << endl;
    }

private:
    int var;
};

class B
{
public:
    B() {}
    void fun()
    {
        cout << "fun():" << ex.var << endl; // 访问类 A 中的私有成员
    }

private:
    A ex;
};

int main()
{
    B ex;
    ex.fun(); // fun():10
    return 0;
}

2.21  静态绑定和动态绑定是怎么实现的

静态类型:变量在声明时的类型,是在编译阶段确定的。静态类型不能更改。

动态类型:目前所指对象的类型,是在运行阶段确定的。动态类型可以更改。

静态绑定是指程序在 编译阶段 确定对象的类型(静态类型)。

动态绑定是指程序在 运行阶段 确定对象的类型(动态类型)。

实现:对于类的成员函数,只有虚函数是动态绑定,其他都是静态绑定。

#include <iostream>

using namespace std;

class Base
{
public:
	virtual void fun() { cout << "Base::fun()" << endl;
     }
};
class Derive : public Base
{
public:
	void fun() { cout << "Derive::fun()"; 
    }
};


int main()
{
	Base *p = new Derive(); // p 的静态类型是 Base*,动态类型是 Derive*
    p->fun(); // fun 是虚函数,运行阶段进行动态绑定
	return 0;
}
/*
运行结果:
Derive::fun()
*/

2.22 c++构造函数和析构函数能不能调用虚函数

·从语法上讲,调用完全没有问题。

·但是从效果上看,往往不能达到需要的目的。

#include<iostream>
using namespace std;
 
class Base
{
public:
    Base()
    {
       Function();
       cout << "Base constructor" << endl; 
    }
 
    virtual void Function()
    {
        cout << "Base::Fuction" << endl;
    }
 
	virtual ~Base()
	{
		Function();
		cout << "Base destructor" << endl; 
	}
};
 
class A : public Base
{
public:
    A()
    {
      Function();
      cout << "A constructor" << endl; 
    }
 
    virtual void Function()
    {
        cout << "A::Function" << endl;
    }
 
	virtual ~A()
	{
		Function();
		cout << "A destructor" << endl; 
	}
};
 
int main()
{
    Base* a = new Base;
	delete a;
 
	cout << "-------------------------" <<endl;
 
	Base* b = new A;//语句1
	delete b;
}

输出:

 

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

Base 类的构造函数中调用 Base 版本的虚函数,子类A的构造函数中调用子类A版本的虚函数

子类A的析构函数中调用子类A版本的虚函数,Base 类的析构函数中调用 Base 版本的虚函数

那么,为什么说:但是从效果上看,往往不能达到需要的目的?

因为,类设计者可能希望在基类指针指向子类对象时,通过该基类指针调用的虚函数版本应该是子类的虚函数版本。但是,很明显我们看到:

在进入基类构造函数时,调用了基类版本的虚函数
在进入基类虚构函数时,调用了基类版本的虚函数

这可能与预期不相符合。

毕竟,如果我们需要这样的结果的话,为什么不在一开始就把该虚函数声明为普通函数呢?这样可以达到同样的效果:

将该虚函数声明为普通函数的话,同样达到了上述实验一样的效果
这便是所谓的:

但是从效果上看,往往不能达到需要的目的
 

3.泛型编程

3.1 什么是模板?如何实现?模板分类?

模板:创建类或者函数的蓝图或者公式,分为函数模板和类模板。

实现方式:模板定义以关键字 template 开始,后跟一个模板参数列表。

·模板参数列表不能为空;

·模板类型参数前必须使用关键字 class 或者 typename,在模板参数列表中这两个关键字含义相同,可互换使用。

template <typename T, typename U, ...>

模板分类

1.函数模板:通过定义一个函数模板,可以避免为每一种类型定义一个新函数。

·对于函数模板而言,模板类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明类型转换

·函数模板实例化:当调用一个模板时,编译器用函数实参来推断模板实参,从而使用实参的类型来确定绑定到模板参数的类型

#include<iostream>

using namespace std;

template <typename T>
T add_fun(const T & tmp1, const T & tmp2){
    return tmp1 + tmp2;
}

int main(){
    int var1, var2;
    cin >> var1 >> var2;
    cout << add_fun(var1, var2);

    double var3, var4;
    cin >> var3 >> var4;
    cout << add_fun(var3, var4);
    return 0;
}

2.类模板:类似函数模板,类模板以关键字 template 开始,后跟模板参数列表。但是,编译器不能为类模板推断模板参数类型,需要在使用该类模板时,在模板名后面的尖括号中指明类型。

#include <iostream>

using namespace std;

template <typename T>
class Complex
{
public:
    //构造函数
    Complex(T a, T b)
    {
        this->a = a;
        this->b = b;
    }

    //运算符重载
    Complex<T> operator+(Complex &c)
    {
        Complex<T> tmp(this->a + c.a, this->b + c.b);
        cout << tmp.a << " " << tmp.b << endl;
        return tmp;
    }

private:
    T a;
    T b;
};

int main()
{
    Complex<int> a(10, 20);
    Complex<int> b(20, 30);
    Complex<int> c = a + b;

    return 0;
}

3.2 什么是可变参数模板

可变参数模板:接受可变数目参数的模板函数或模板类。将可变数目的参数被称为参数包,包括模板参数包函数参数包

·模板参数包:表示零个或多个模板参数

·函数参数包:表示零个或多个函数参数

用省略号来指出一个模板参数或函数参数表示一个包,在模板参数列表中,class… 或 typename… 指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。当需要知道包中有多少元素时,可以使用 sizeof… 运算符。

template <typename T, typename... Args> // Args 是模板参数包
void foo(const T &t, const Args&... rest); // 可变参数模板,rest 是函数参数包
#include <iostream>

using namespace std;

template <typename T>
void print_fun(const T &t)
{
    cout << t << endl; // 最后一个元素
}

template <typename T, typename... Args>
void print_fun(const T &t, const Args &...args)
{
    cout << t << " ";
    print_fun(args...);
}

int main()
{
    print_fun("Hello", "wolrd", "!");
    return 0;
}
/*运行结果:
Hello wolrd !

*/


说明:可变参数函数通常是递归的,第一个版本的 print_fun 负责终止递归并打印初始调用中的最后一个实参(最后一个实参如果不是绑定第一个版本,仍去调用第二个版本的话会陷入死循环)。第二个版本的 print_fun 是可变参数版本,打印绑定到 t 的实参,并用来调用自身来打印函数参数包中的剩余值。

3.3 什么是模板特化?为什么特化?

模板特化的原因:模板并非对任何模板实参都合适、都能实例化,某些情况下,通用模板的定义对特定类型不合适,可能会编译失败,或者得不到正确的结果。因此,当不希望使用模板版本时,可以定义类或者函数模板的一个特例化版本。

模板特化:模板参数在某种特定类型下的具体实现。分为函数模板特化类模板特化

·函数模板特化:将函数模板中的全部类型进行特例化,称为函数模板特化。

·函数模板特化:将函数模板中的全部类型进行特例化,称为函数模板特化。

特化分为全特化和偏特化:

·全特化:模板中的模板参数全部特例化。

·偏特化:模板中的模板参数只确定了一部分,剩余部分需要在编译器编译时确定。

要区分下函数重载与函数模板特化:定义函数模板的特化版本,本质上是接管了编译器的工作,为原函数模板定义了一个特殊实例,而不是函数重载,函数模板特化并不影响函数匹配。

#include <iostream>
#include <cstring>

using namespace std;
//函数模板
template <class T>
bool compare(T t1, T t2)
{
    cout << "通用版本:";
    return t1 == t2;
}

template <> //函数模板特化
bool compare(char *t1, char *t2)
{
    cout << "特化版本:";
    return strcmp(t1, t2) == 0;
}

int main(int argc, char *argv[])
{
    char arr1[] = "hello";
    char arr2[] = "abc";
    cout << compare(123, 123) << endl;
    cout << compare(arr1, arr2) << endl;

    return 0;
}
/*
运行结果:
通用版本:1
特化版本:0
*/
#include <deque>
#include <string>
#include <cassert>

template<>//类模板特化
class Stack<std::string>{
private:
    std::deque<std::string> elems; 

public:
    void push(std::string const&); 
    void pop(); // pop element
    std::string const& top() const; 
    bool empty() const {
        return elems.empty();
    }
};

void Stack<std::string>::push(std::string const& elem)
{
    elems.push_back(elem); 
}

void Stack<std::string>::pop()
{
    assert(!elems.empty());
    elems.pop_back();
}

std::string const& Stack<std::string>::top() const
{
    assert(!elems.empty());
    return elems.back(); 
}

4.STL

4.1 STL组成

容器、迭代器、仿函数、算法、分配器、配接器

4.2 map和unorderd_map异同

unordered_mapmap
查找,average:O(1),Worst:O(n)恒定long(n)
插入同上log(n)+平衡二叉树用时
删除同上log(n)+平衡二叉树用时
是否排序不排序排序
实现方法哈希表红黑树
适用场景查找操作频率较高场景要求结果按key排序场景

4.3 list和vector异同

即链表(list)和顺序表(vector)的差别,略

4.4 简述STL迭代器

1.迭代器Iterator(迭代器)模式又称 Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。或者这样说可能更容易理解:Iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。由于 Iterator 模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如STL的list、 vector、 stack等容器类及ostream_iterator等扩展 iterator

2.迭代器和指针的区别迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,通过重载了指针的一些操作符,->、*、++、-等。迭代器封装了指针,是一个“可遍历STL(Standard TemplateLibrary)容器内全部或部分元素”的对象, 本质是封装了原生指针,是指针概念的一种提升(lift),提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,—-等操作。迭代器返回的是对象引用而不是对象的值,所以 cout 只能输出迭代器使用*取值后的值而不能直接输出其自身。

3.迭代器产生原因Iterator 类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。

4.5 利用STL迭代器删除元素时效问题讨论

1.对于序列容器vector, deque来说,使用erase(itertor)后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动一个位置,但是erase会返回下一个有效的迭代器;

2.对于关联容器map set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。

3,对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此list不能使用erase删除元素,list删除元素需要借助erase_after参考

4.6 STL中resize和reserve区别

resize():改变当前容器内含有元素的数量(size()),eg: vector<int>v; v.resize(len);v的size变为len,如果原来v的size小于len,那么容器新增(len-size)个元素,元素的值为默认为0.当v.push_back(3);之后,则是3是放在了v的末尾,即下标为len,此时容器size为len+1;

reserve():改变当前容器的最大容量(capacity),它不会生成元素,只是确定这个容器允许放入多少对象,如果reserve(len)的值大于当前的capacity(),那么会重新分配一块能存len个对象的空间,然后把之前v.size()个对象通过copy construtor复制过来,销毁之前的内存;

只有当容器内元素数(size)大于capcity时,容器才会改变地址。

4.7 STL中erase和remove区别

· erase是删除指定位置的元素或者指定区域内的所有元素

· remove是删除和指定元素值相同的所有元素(remove需要和erase搭配使用才能实现完整的删除功能)

erase错误用法:

	vector<int> vec = { 1,2,3,4,5,6,7 };

	vector<int>::iterator itr = vec.begin();
	while (itr != vec.end())
	{
		if (*itr == 1)
		{
			vec.erase(itr);   //第一处错误,itr失效,称为野指针
		}
		itr++;                //第二处错误,如果itr = erase返回值,这里itr就会自加两次
	}

 上述例子编译不会通过,因为被删除的迭代器会失效,变成一个野指针,这时再和末尾位置迭代器进行比较会出错,此时正确的做法是将erase的返回值赋值给itr,因为erase会返回被删除元素的下一个元素的迭代器。如果这样改了之后,itr ++ 也需要进行修改,避免自加两次。

故erase正确用法:

	vector<int> vec = { 1,2,3,4,5,6,7 };

	vector<int>::iterator itr = vec.begin();

	while (itr != vec.end())
	{
		if (*itr == 1)
		{
			itr = vec.erase(itr);  
		}
		else
			itr++;                
	}

erase和remove搭配使用实现简便删除元素用法示例:

vector<int> vec = { 1,1,3,4,5,6,7 };

vec.erase(remove(vec.begin(), vec.end(), 1), vec.end());

//输出:3 4 5 6 7

//解释:remove(vec.begin(), vec.end(), 1)将所有1放到vec末尾并返回指向第一个1的迭代器假设为iter,
//再执行erase(iter,vec.end())则是将所有末尾的1删除

4.8 STL中的push和emplac区别

emplace可以直接传入构造对象需要的元素,然后自己调用其构造函数!

eg:

//假设栈内的数据类型是data
class data {
  int a;
  int b;
public:
  data(int x, int y):a(x), b(y) {}
};
vector<data> v;
v.emplace(1,2)

emplace这样接受新对象的时候,自己会调用其构造函数生成对象然后放在容器内(比如这里传入了1,2,它则会自动调用一次data(1,2))。相当于emplace直接把原料拿进家,造了一个。而push是造好了之后,再复制到自己家里,多了复制这一步。所以emplace相对于push,更节省内存
 

5.C++底层

5.1 c++程序编译过程

1.编译预处理

预处理又称为预编译,是做些代码文本替换工作。编译器执行预处理指令(以#开头,例如#include),这个过程会拷贝#include 包含的文件代码,得到不包含#指令的 .i 文件。然后进行#define 宏定义的替换 , 处理条件编译指令 (#ifndef #ifdef #endif)等。

2.编译优化

通过预编译输出的.i文件中,只有常量:数字、字符串、变量的定义,以及c语言的关键字:main、if、else、for、while等。这阶段要做的工作主要是,通过语法分析和词法分析,确定所有指令是否符合规则,之后翻译成汇编代码。这个过程将.i文件转化位 .s 文件。

3.汇编

汇编过程就是把汇编语言翻译成目标机器指令的过程,目标文件中存放的也就是与源程序等效的目标的机器语言代码(即目标机器指令)

目标文件由段组成,通常至少有两个段:

代码段:包换主要程序的指令。该段是可读和可执行的,一般不可写

数据段:存放程序用到的全局变量或静态数据。可读、可写、可执行。

这个过程将.s文件转化成 .o 文件。

4.连接过程

由汇编程序生成的目标文件并不能立即就执行,还要通过链接过程。

原因:

1).某个源文件调用了另一个源文件中的函数或常量.

2).在程序中调用了某个库文件中的函数.

故链接程序的主要工作就是将有关的目标文件连接起来。

这个过程将.o文件转化成可执行的文件 .exe

6.C++新特性

6.1智能指针中使用make_shared初始化智能指针优缺点

优点1:提高性能std::make_shared申请一个单独的内存块来同时存放所涉及的对象控制块。这个优化减少了程序的静态大小,因为代码只包含一次内存分配的调用,并且这会加快代码的执行速度,因为内存只分配了一次。另外,使用std::make_shared消除了一些控制块需要记录的信息,这样潜在地减少了程序的总内存占用。

优点2:异常安全,std::make_shared可以防止内存泄露

缺点1:构造函数是保护或私有时,无法使用 make_shared,std::make_shared消除虽好, 但也存在一些问题, 比如, 当我想要创建的对象没有公有的构造函数时, std::make_shared就无法使用了

缺点2:对象的内存可能无法及时回收,make_shared 只分配一次内存, 这看起来很好. 减少了内存分配的开销. 问题来了: e.g. weak_ptr 会保持控制块(强引用, 以及弱引用的信息)的生命周期, 也连带着保持了对象分配的内存, 只有最后一个 weak_ptr 离开作用域时, 内存才会被释放. 原本强引用减为 0 时就可以释放的内存, 现在变为了强引用、弱引用都减为 0 时才能释放, 这延迟了内存释放的时间. 这对于内存要求高的场景来说, 是一个需要注意的问题.

参考链接:

https://blog.csdn.net/Awesomewan/article/details/123948929?spm=1001.2014.3001.5506

https://blog.csdn.net/baidu_16370559/article/details/123330995

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值