c++基础知识

内存分布

 

  1. 栈:非new获取的变量,编译器分配释放

  2. 堆:new获取,需手动释放

  3. 全局数据区:全局变量&static变量存储区域,.data存储初始化(包含读写data区、只读数据区),.bss存储未初始化(或初始化为0)内容

  4. 文字常量区:常量字符串,编译时确定

  5. 代码区:存储程序代码

    img

img

函数调用栈分布:

ebp:栈底指针 esp:栈顶指针 eip:寄存器要读的指令地址

ret指令=pop+jump

详解可参考:https://www.cnblogs.com/sddai/p/9762968.html

虚函数、虚表

  1. 虚表为类所有、全局数据区

  2. 虚表指针为对象所有

  3. 构造函数不可为虚函数、析构函数应该为虚函数

 

多态、继承的内存大小

  1. 空类大小为1、编译器为空类插入1字节的char,以使该类对象在内存得以配置一个地址

 
 
 
 
 
 
 
class Base {
  
};
 
  1. 单一继承(派生类新增虚函数 、 不新增虚函数 结果一致,不会增加虚表、只是虚表新增一个虚函数指针

 
 
 
 
 
 
 
class Base {
  public:
    void f();
    virtual void g();   // 虚表指针,位于类头部、一个虚函数
  private:
    int a;
    static int b;       // 类所有,不占空间
    char c;             // padding加字节让类符合对齐原则
}
32大小为12、64位大小16
  
class Derived : public Base {
  public:
    virtual void h();   // 两个虚函数、一个虚表指针(这个虚函数只有子类有、父类没有)
  private:
    int c;
    int d;
}
32位大小为20、64位大小为24
注意:单一继承、派生类与基类公用虚表指针(都位于类对象初始为0的位置)
 
  1. 单一虚继承(派生类不新增虚函数)

 
 
 
 
 
 
 
class Base {
  public:
    void f();
    virtual void g();
    virtual void h();
  private:
    int a;
    int b;
};
class Derived : virtual public Base {
  public:
    virtual void g();
  private:
    int c;
    int d;
}
 

img

  1. 单一虚继承(派生类新增虚函数)

 
 
 
 
 
 
 
class Base {
  public:
    void f();
    virtual void g();
    virtual void h();
  private:
    int a;
    int b;
};
class Derived : virtual public Base {
  public:
    virtual void g();
    virtual void h_derived();
  private:
    int c;
    int d;
};
 

img

从上图与单一的虚拟继承(派生类不新增虚函数)相比,Derived object size是28,相比增大了4个byte,而这4个byte正是Derived中被编译器新增安插的vfptr(上图红色框图中)。我们看到Derived新增的虚函数h_derived并没有直接放入Derived::$vftable@Base@中,而是新增加vftable(Derived::$vftable@Derived@)。

  1. 多继承(派生类不新增虚函数)

 
 
 
 
 
 
 
class Base {
  public:
    virtual void virtualFunction();
  private:
    int a;
    int b;
};
class Derived1 : public Base {
  public:
    virtual void virtualFunction();
    virtual void virtualDerived1Function();
  private:
    int c;
};
class Derived2 : public Base {
  public:
    virtual void virtualFunction();
    virtual void virtualDerived2Function();
  private:
    int d;
};
class Derived : public Derived1, public Derived2 {
  public:
    virtual void virtualFunction();
  private:
    int e;
}
 

img

img

img

img

我们看到这里有两份vftable,分别针对Derived1与Derived2

  1. 多继承(派生类新增虚函数)

 
 
 
 
 
 
 
class Base {
  public:
    virtual void virtualFunction();
  private:
    int a;
    int b;
};
class Derived1 : public Base {
  public:
    virtual void virtualFunction();
    virtual void virtualDerived1Function();
  private:
    int c;
};
class Derived2 : public Base {
  public:
    virtual void virtualFunction();
    virtual void virtualDerived2Function();
  private:
    int d;
};
class Derived : public Derived1, public Derived2 {
  public:
    virtual void virtualFunction();
    virtual void virtualDerivedFunction();
  private:
    int e;
}
 

img

我们看到唯一差异点:上图红色框图中,Derived新增的虚函数只有在vftable@Derived1的虚函数表中有,并不会在vftable@Derived2的虚函数表。也就是说Derived并没有额外新增vfptr和vftable,而是直接引用来自继承列表中的第一个父类Derived1中的vftable。

 

  1. 虚拟多继承(派生类新增虚函数)

 
 
 
 
 
 
 
class Base {
  public:
    virtual void virtualFunction();
  private:
    int a;
    int b;
};
class Derived1 : public Base {
  public:
    virtual void virtualFunction();
    virtual void virtualDerived1Function();
  private:
    int c;
};
class Derived2 : public Base {
  public:
    virtual void virtualFunction();
    virtual void virtualDerived2Function();
  private:
    int d;
};
class Derived : public Derived1, public Derived2 {
  public:
    virtual void virtualFunction();
    virtual void virtualDerivedFunction();
  private:
    int e;
};
 

img

 

img

img

img

img

 

最后总结:https://blog.csdn.net/u014558668/article/details/77476448

1.当类含有虚函数时(包括继承而来的虚函数)都有虚函数表指针vfptr和虚函数表vftable。

2.单一的普通继承(非虚继承),子类只有一个vfptr(子类从父类继承下来),并指向自身的vftable(包含:继承自父类的虚函数、子类覆盖父类的虚函数、子类新增的虚函数)。(子类不新增虚指针)

3.多重继承(非虚继承),可能存在多个的基类vfptr与vftable。如果子类有新增虚函数,编译器并不会为子类额外新增自己的vfptr,而是直接引用来自继承列表中的第一个父类中的vfptr,并在此vfptr指向的vftable中增加子类新增的虚函数。(子类不新增虚指针)

4.如果是单一的虚拟继承,则子类对象中会被编译器安插一个指向vbtable(记录virtual base class在内存布局中与vbptr的距离偏移)的vbptr。当子类中有新增虚函数时,编译器则会给子类对象中再新增一个vfptr,这个vfptr指向新增vftable(这张vftable只含有子类自己新增的虚函数),而subobject中的vfptr指向的vftable(也就是virtual base class中的vfptr指向的vftable)中存储的是子类从父类继承或子类覆盖父类的虚函数地址。

5.对于钻石型多重继承,可以按照以上原则类推。

 

说明:C++对象内存布局,不同编译器实现细节不同,本文所有测试结果都依赖VS2013。

 

模板

模板、特化、偏特化,执行顺序:特化>偏特化>一般情况

 
 
 
 
 
 
 
#include <iostream>
using namespace std;
// 一般化设计
template <class T, class T1>
class TestClass
{
public:
     TestClass()
     {
          cout<<"T, T1"<<endl;
     }
};
// 针对普通指针的偏特化设计
template <class T, class T1>
class TestClass<T*, T1*>
{
public:
     TestClass()
     {
          cout<<"T*, T1*"<<endl;
     }
};
// 针对const指针的偏特化设计
template <class T, class T1>
class TestClass<const T*, T1*>
{
public:
     TestClass()
     {
          cout<<"const T*, T1*"<<endl;
     }
};
// 特化
template <>
class TestClass<int, int> {
public:
    TestClass()
    {
          cout << "int, int" << endl;
    }
}
int main()
{
     TestClass<int, char> obj;
     TestClass<int *, char *> obj1;
     TestClass<const int *, char *> obj2;
     return 0;
}
 

 

 

 

编译器自动生成的函数

c++

 
 
 
 
 
 
 
class Dog {
// C++03
  Dog(); // 默认构造函数
  Dog(const Dog&); // 拷贝构造函数
  ~Dog() // 析构函数
  // 拷贝赋值运算符
  Dog& operator=(const Dog&);
// C++11 新增
  Dog(Dog&&); // 移动构造函数
  // 移动赋值运算符
  Dog& operator=(Dog&&);
};
 

c++编译器默认会生成的六种类相关函数

  1. 默认构造函数:只在用户没有定义其它类型构造函数时才会生成

  2. 拷贝构造函数:只有用户没有定义 移动构造函数(5) 和 移动赋值运算符(6) 时,才会生成

  3. 拷贝赋值运算符:只有用户没有定义 移动构造函数(5) 和 移动赋值运算符(6) 时,才会生成

  4. 析构函数:无限制

  5. 移动构造函数:只有用户没有定义 拷贝构造函数(2), 拷贝赋值运算符(3), 析构函数(4) 和 移动赋值运算符(6) 时,才会生成

  6. 移动赋值运算符:只有用户没有定义 拷贝构造函数(2), 拷贝赋值运算符(3), 析构函数(4) 和 移动构造函数(5) 时,才会生成

 

static关键字作用

  1. 静态成员变量:类拥有、存储在全局数据区、类外初始化(sizeof不计算这部分内存)

  2. 静态成员函数:类拥有、只能访问static变量&函数、无this指针

  3. 静态全局变量:文件可见(不同文件可同名)、存储在全局数据区

  4. 静态局部变量:只在声明时初始化一次、存储在全局数据区

  5. 静态函数:文件可见(不同文件可同名)

const关键字作用

  1. const全局变量、局部变量:不可更改、定义时初始化

  2. const修饰指针:看const和*的位置,const在左侧:指针所指内容不可变(常量指针),const在右:指针本身不可变(指针常量

  3. const修饰函数

    • 参数为复制型,无意义

    • 参数为引用,不可更改

    • 参数为指针,参考const修饰指针

    • 修饰返回值,不可更改

  4. Const类成员变量:不可更改、初始化列表赋值

  5. Const类成员函数:不可修改类成员变量(指针例外,可以修改指针指向的内容)、可被不包含const的重载

  6. const类对象:只可调用const成员函数

 

sizeof和strlen区别

  1. sizeof为操作符,可接变量或数据类型,编译已有结果,对数组操作不退化(只算栈大小、因此static不计算在内)

  2. strlen为库函数,只可接字符串,运行期才有结果,对数组操作为指针大小

 

malloc和new的区别

  1. malloc为函数,仅分配内存,不调用构造函数、void*

  2. new为操作符,可重载,调用构造函数、类型*

 

对齐原则(结构体或类空间分布)

  1. 结构体变量的首地址能够被其最宽基本类型成员的大小所整除

  2. 结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,如需要时,编译器会在成员之间加上填充字节

  3. 结构体的总大小为结构体最宽基本类型成员大小的整数倍

  4. 有效对齐值=min{自身对齐值,当前指定的pack值}

  • 使用伪指令#pragma pack(n):C编译器将按照n个字节进行对齐;

  • 使用伪指令#pragma pack():取消自定义的字节对齐方式;

  • 另外,还有GCC的特有语法

    attribute((aligned(n))):让所作用的结构体成员对齐在n字节自然边界上,如果结构体中有成员的长度大于n,则按照最大成员的长度来对齐;

    attribute((packed)):取消结构体在编译过程中的优化对齐,按照实际占用的字节数进行对齐。

 

c/c++ struct区别

  1. c语言struct无继承、无函数、无访问修饰符

  2. c++都有

 

extern ”C“

  1. c++支持c编译

  2. c语言不支持函数重载

 

struct和class区别

  1. 默认继承权限不同,class-private,struct-public

  2. 默认访问权限不同,class-private,struct-public

  3. struct不支持模板,为了兼容c代码

 

右值引用

c++ primer解释:

一个表达式是左值还是右值,取决于我们使用的是它的值还是它在内存中的位置(作为对象的身份)。也就是说一个表达式具体是左值还是右值,要根据实际在语句中的含义来确定。例如

<!--左右值-->

有一个重要的原则,即

  • 在大多数情况下,需要右值的地方可以用左值来替代,但

  • 需要左值的地方,一定不能用右值来替代

又有一个重要的特点,即

  • 左值存放在对象中,有持久的状态;而

  • 右值要么是字面常量,要么是在表达式求值过程中创建的临时对象,没有持久的状态

<!--左右值引用-->

有了右值才有右值引用

  • 右值必须绑定右值引用或常引用,右值引用必须绑定右值

  • 函数返回、表达式结果大多为右值(++i为左值)

不论是左值引用还是右值引用,都有

  • 当引用作为变量被保存下来,那么它是左值;否则

  • 它是右值。

<!--右值引用的作用-->

Person bar = foo() 发生了什么

  • 从函数返回值中得到临时对象 rhs

  • 销毁 bar 中的资源(delete resource_;);

  • rhs 中的资源拷贝一份,赋值给 bar 中的资源(resource_ = new Resource(*(rhs.resource_)););

  • 销毁 rhs 这一临时对象。

  1. 作用1:移动构造函数、移动复制符,增加了效率性能

  2. 作用2:std::forward实现完美转发

  3. 作用3:移动迭代器std::make_move_iterator,解决unique_ptr这种只有移动构造函数、没有拷贝构造函数

完整文章:https://liam.page/2016/12/11/rvalue-reference-in-Cpp/

<!--通用引用-->

与右值引用极度相似,但是必须要满足两个条件:

  1. 必须格式是T&&

  2. 必须有类型推到,一般是auto&&、模板参数两种情况(模板不全是,需分析)

    • 声明为auto&&的变量就是universal references

    •  
       
       
       
       
       
       
      class vector<Widget, allocator<Widget>>{
      public:
        void push_back(Widget&& x); //rvalue reference
        ...
      };
      template<class T,class Allocator=allocator<T>>
      class vector{
      public:
        template <class... Args>
        void emplace_back(Args&&... args);  // 通用引用
        ...
      };
       

在右值引用上使用std::move 在通用引用上使用std::forward

完整文章:https://www.kancloud.cn/kangdandan/book/169997

 

四种转换类型

  1. static_cast:强制类型转换,无限制,类似于c中()强制转换

  2. const_cast:转换表达式的const属性

  3. dynamic_cast:运行时识别指针或引用,一般用户父类与子类之间,向上转换和static_cast一致,向下转换有类型安全检查(比如实际为父类型,转换为子类型,会通不过检查返回空指针)

  4. reinterpret_cast:把二进制重新解释,如字符和int转化,int和float转换

 

C++智能指针

RAII:资源获取即初始化技术,在构造函数的时候申请空间,而在析构函数(在离开作用域时调用)的时候释放空间

  1. Unique_ptr:独占指针,移动构造&赋值

     
     
     
     
     
     
     
    template<typename T, typename ...Args>
    std::unique_ptr<T> make_unique(Args&& ...args ) {
      return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
    }
     
  2. shared_ptr:引用计数,为0则释放资源:make_shared

  3. Weak_ptr:配合shared_ptr,解决循环引用的问题

 

自己实现unique_ptr:

c++

 
 
 
 
 
 
 
template<typename T>
class MyUniquePtr
{
public:
   explicit MyUniquePtr(T* ptr = nullptr)   // explict显示声明构造函数
        :mPtr(ptr)
    {}
    ~MyUniquePtr()
    {
        if(mPtr)
            delete mPtr;
    }
    // 移动构造函数,移动操作符
    MyUniquePtr(MyUniquePtr &&p) noexcept; 
    MyUniquePtr& operator=(MyUniquePtr &&p) noexcept;
    // 去除掉拷贝构造函数、拷贝赋值
    MyUniquePtr(const MyUniquePtr &p) = delete;
    MyUniquePtr& operator=(const MyUniquePtr &p) = delete;
    // 提供的对外操作
    T& operator*() const noexcept {return *mPtr;}
    T* operator->() const noexcept {return mPtr;}
    T* get() const noexcept {return mPtr;}
    // 用来做if(p)这种判断,默认是true
    explicit operator bool() const noexcept{return mPtr;}
    // 按需分配、非重要功能
    void reset(T* q = nullptr) noexcept
    {
        if(q != mPtr){
            if(mPtr)
                delete mPtr;
            mPtr = q;
        }
    }
    T* release() noexcept
    {
        T* res = mPtr;
        mPtr = nullptr;
        return res;
    }
    void swap(MyUniquePtr &p) noexcept
    {
        using std::swap;
        swap(mPtr, p.mPtr);
    }
private:
    T* mPtr;
};
template<typename T>
MyUniquePtr<T>& MyUniquePtr<T>::operator=(MyUniquePtr &&p) noexcept
{
    if(*this != p)
    {
        if(mPtr)
            delete mPtr;
        mPtr = p.mPtr;
        p.mPtr = NULL;
    }
    return *this;
}
template<typename T>
MyUniquePtr<T> :: MyUniquePtr(MyUniquePtr &&p) noexcept : mPtr(p.mPtr)
{
    p.mPtr == NULL;
}
以下情形鼓励使用noexcept:为了实现运行时检测,编译器创建额外的代码,然而这会妨碍程序优化。
移动构造函数(move constructor)
移动分配函数(move assignment)
析构函数(destructor)。这里提一句,在新版本的编译器中,析构函数是默认加上关键字noexcept的。下面代码可以检测编译器是否给析构函数加上关键字noexcept。
 

 

手撕shared_ptr、weak_ptr实现:

https://blog.csdn.net/dong_beijing/article/details/79504591

 

设计一个不能被继承的类

核心点:私有化构造函数、析构函数

 

vector底层原理

  1. reserve预留空间、不改变大小,如何释放空间?(push_back会增大空间,只有析构会释放)

    vector<**int**>().swap(arr); //交换后

    {

    vector<int> tmp;

    tmp.swap(arr);

    }

     

  2. resize改变大小、构造对象

 

友元

友元函数

  1. 可为全局函数、类成员函数(不能为私有函数)

  2.  
     
     
     
     
     
     
    #include<iostream>
    using namespace std;
    class CCar;  //提前声明CCar类,以便后面的CDriver类使用
    class CDriver
    {
    public:
        void ModifyCar(CCar* pCar);  //改装汽车
    };
    class CCar
    {
    private:
        int price;
        friend int MostExpensiveCar(CCar cars[], int total);  //声明友元
        friend void CDriver::ModifyCar(CCar* pCar);  //声明友元
    };
    void CDriver::ModifyCar(CCar* pCar)
    {
        pCar->price += 1000;  //汽车改装后价值增加
    }
    int MostExpensiveCar(CCar cars[], int total)  //求最贵气车的价格
    {
        int tmpMax = -1;
        for (int i = 0; i<total; ++i)
            if (cars[i].price > tmpMax)
                tmpMax = cars[i].price;
        return tmpMax;
    }
    int main()
    {
        return 0;
    }
     

 

友元类

  1. 一个类 A 可以将另一个类 B 声明为自己的友元,类 B 的所有成员函数就都可以访问类 A 对象的私有成员

  2.  
     
     
     
     
     
     
    class CCar
    {
    private:
        int price;
        friend class CDriver;  //声明 CDriver 为友元类
    };
    class CDriver
    {
    public:
        CCar myCar;
        void ModifyCar()  //改装汽车
        {
            myCar.price += 1000;  //因CDriver是CCar的友元类,故此处可以访问其私有成员
        }
    };
    int main()
    {
        return 0;
    }
     

 

STL 内存池原理

理念:操作系统内存分配很耗时, 一次分配, 多次使用, 自然而然提高了效率

img

数据结构:一个指针数组(大小为16,按照8-128字节分)、多个自由链表

 
 
 
 
 
 
 
private:
    static const int Align = 8;
    static const int MaxBytes = 128;
    static const int NumberOfFreeLists = MaxBytes / Align;
    static const int NumberOfAddedNodesForEachTime = 20;
    union node {
      union node *next;
      char client[1];
    };
    static obj *freeLists[NumberOfFreeLists];`
 

使用union的理由:

基本思路:

  1. size>128,直接malloc分配,否则找到大小合适的链表,分出一个节点

  2. size>128,直接free释放,否则将内存放置在大小合适的链表中

流程:

  1. 使用allocate向内存池请求size大小的内存空间, 如果需要请求的内存大小大于128bytes, 直接使用malloc.

  2. 如果需要的内存大小小于128bytes, allocate根据size找到最适合的自由链表.

  a. 如果链表不为空, 返回第一个node, 链表头改为第二个node.

  b. 如果链表为空, 使用blockAlloc请求分配node.

    x. 如果内存池中有大于一个node的空间, 分配竟可能多的node(但是最多20个), 将一个node返回, 其他的node添加到链表中.

    y. 如果内存池只有一个node的空间, 直接返回给用户.

    z. 若果如果连一个node都没有, 再次向操作系统请求分配内存.

      ①分配成功, 再次进行b过程

      ②分配失败, 循环各个自由链表, 寻找空间

        I. 找到空间, 再次进行过程b

        II. 找不到空间, 抛出异常(代码中并未给出, 只是给出了注释)

  1. 用户调用deallocate释放内存空间, 如果要求释放的内存空间大于128bytes, 直接调用free.

  2. 否则按照其大小找到合适的自由链表, 并将其插入.

详解文档:https://www.cnblogs.com/nzhl/p/5753728.html

 

select、poll、epoll区别

https://imageslr.com/2020/02/27/select-poll-epoll.html

select

  1. 监控的socket最大为FD_SIZE,大小受限制

  2. 只知道有数据返回,却不知道是哪个socket、只能无差别轮询、复杂度O(N)

  3. socket数组频繁在内核空间与用户空间切换(每次轮询都要重新更新数组

  4. 原理是按位与来查看状态、数组结构

15732186159520

poll

  1. 与select基本一致,只是换成了链表结构,去除了大小限制

epoll

  1. 水平触发、边缘触发(只通知一次,效率高,而且可以去掉一些不关心的文件描述符)为什么 epoll边缘触发模式不能使用阻塞 I/O?很显然,边缘触发模式需要循环读/写一个文件描述符的所有数据。如果使用阻塞 I/O,那么一定会在最后一次调用(没有数据可读/写)时阻塞,导致无法正常结束

  2. 无大小限制,ready list为双链表结构O(1),监控的描述符为红黑树结构,为O(N),还有一个存储进程的结构(用来通知进程)

  3. 明确知道哪些socket有返回,无需轮询;也不需要每次都重新把描述符加入数组

  4. mmap映射到同一块内存区,减少copy

img

posted @ 2022-05-05 00:40  cosinehzq  阅读(54)  评论(0)    收藏  举报