C++学习知识点整理

指针跟引用的区别

指针是变量,存储的地址,可以为空;
引用是对象的别名,不可以为空,必须初始化,不能改变;
sizeof以及++操作结果不同
参数传递不同,返回动态内存分配的对象时必须用指针,引用会导致内存泄漏

堆与栈的区别

栈由系统自动分配释放,连续空间,内存地址由高到低,空间限制,分配速度快
堆手动分配释放,不连续空间,速度慢,有碎片,内存地址由低到高

浅拷贝与深拷贝

浅拷贝只拷贝指向对象的指针,不复制对象本身(浅拷贝在对象调用析构函数时会崩溃,因为两次调用析构函数释放的是同一块内存,导致内存泄漏);
深拷贝创造一个相同的对象,新对象与原对象不共享内存

零拷贝技术

零拷贝浅析
零拷贝主要的任务就是避免CPU将数据从一块存储拷贝到另外一块存储,主要就是利用各种零拷贝技术,避免让CPU做大量的数据拷贝任务,减少不必要的拷贝,或者让别的组件来做这一类简单的数据传输任务,让CPU解脱出来专注于别的任务。这样就可以让系统资源的利用更加有效。

什么情况下会调用拷贝构造函数?

1.用类的一个对象去初始化另一个对象时;
2.函数形参为类的对象,值传递(引用不会);
3.函数返回值是类的对象(引用不会)。

C++11新特性

  1. nullptr替代NULL(避免函数重载时发生混乱);

  2. for循环基于范围的迭代写法

// & 启用了引用
for(auto &i : arr) {    
    std::cout << i << std::endl;
}
  1. 引入智能指针shared_ptr/weak_ptr/function/bind/lambda/unique_ptr/
  2. vector<unique_ptr>
  3. 生成随机数的 mt19937(生成int范围的整数)
  4. 增加右值引用

C++四种强制转换

  1. static_cast
    与c中的(type)功能一致,转换的时候不会检查类型来保证转换的安全性
  2. const_cast
    将const类型指针转换为非const类型指针
  3. dynamic_cast
    只能转换指针、引用、void*,用于含有虚函数的类
  4. reinterpret_cast<T*>
    对数据的二进制形式重新解释,但是不改变其值。T必须为一个指针、引用、算数类型,最普通的用途就是在函数指针类型之间进行转换。
    把一个指向下面函数的指针存入funcPtrArray数组:
    int doSomething();
    你不能不经过类型转换而直接去做,因为doSomething函数对于funcPtrArray数组来说有一个错误的类型。在FuncPtrArray数组里的函数返回值是void类型,而doSomething函数返回值是int类型。
    funcPtrArray[0] = &doSomething;//错误!类型不匹配
    reinterpret_cast可以让你迫使编译器以你的方法去看待它们:
    funcPtrArray[0] = reinterpret_cast(&doSomething);
    转换函数指针的代码是不可移植的(C++不保证所有的函数指针都被用一样的方法表示),在一些情况下这样的转换会产生不正确的结果

多态

特殊的多态:
强制多态:通过将一种类型数据转换为另一种类型数据实现;
重载多态:指给同一个名字赋予不同的含义,例如函数重载,运算符重载
一般的多态:
包含多态:通过虚函数实现
类型参数多态:通过函数模板和类模板

虚函数的实现原理

参考

static关键字的作用

四种作用

RAII(resource acquisition is initialization)

资源获取即初始化(RAII, Resource Acquisition Is Initialization)是指,当你获得一个资源的时候,不管这个资源是对象、内存、文件句柄或者其它什么,你都会在一个对象的构造函数中获得它,并且在该对象的析构函数中释放它。实现这种功能的类,我们就说它采用了"资源获取即初始化(RAII)"的方式。这样的类常常被称为封装类。

smart pointer 四个智能指针的理解

智能指针的作用是管理一个指针,在函数结束时自动释放内存空间;
操作方法:通过类的构造函数将类外需要管理的指针传进来,在析构函数中释放该指针空间。

auto_ptr 是C++98标准引入,任何情况下不要使用

  1. 不要使用auto_ptr保存一个非动态开辟空间的指针,因为在作用域结束的时候,会执行智能指针的析构函数,释放这块空间,但非动态的空间又无法释放;
  2. 不要使用两个auto_ptr指针指向同一个指针,具体原因上面解释过;
  3. 不要使用auto_ptr指向一个指针数组,因为auto_ptr的析构函数所用的是delete而不是delete[],不匹配;
  4. 不要将auto_ptr储存在容器中,因为赋值和拷贝构造后原指针无法使用。

**unqiue_ptr(scoped_ptr)**防拷贝指针,功能不全
功能
scoped_ptr使用私有拷贝构造函数和赋值运算符,因此不可使用一个对象创建另一个对象,可使用move转移内存资源所有权
shared_ptr 共享指针
引用计数,用于检测对象所管理的指针是否被其他智能指针使用
当管理的指针为双向链表时,造成内存泄漏,成为循环引用。

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

template <typename T>
class SmartPointer{
public:
    SmartPointer(T* ptr){
        ref = ptr;
        ref_count = (unsigned*)malloc(sizeof(unsigned));
        *ref_count = 1;
    }
  //拷贝构造函数  
    SmartPointer(SmartPointer<T> &sptr){
        ref = sptr.ref;
        ref_count = sptr.ref_count;
        ++*ref_count;
    }
    //赋值构造函数
    SmartPointer<T>& operator=(SmartPointer<T> &sptr){
        if (this != &sptr) {
            if (--*ref_count == 0){
                clear();
                cout<<"operator= clear"<<endl;
            }
            
            ref = sptr.ref;
            ref_count = sptr.ref_count;
            ++*ref_count;
        }
        return *this;
    }
    
    ~SmartPointer(){
        if (--*ref_count == 0){
            clear();
            cout<<"destructor clear"<<endl;
        }
    }
    
    T getValue() { return *ref; }
    
private:
    void clear(){
        delete ref;
        free(ref_count);
        ref = NULL; // 避免它成为迷途指针
        ref_count = NULL;
    }
   
protected:    
    T *ref;
    unsigned *ref_count;
};

int main(){
    int *ip1 = new int();
    *ip1 = 11111;
    int *ip2 = new int();
    *ip2 = 22222;
    SmartPointer<int> sp1(ip1), sp2(ip2);
    SmartPointer<int> spa = sp1;
    sp2 = spa; // 注释掉它将得到不同输出
    return 0;
}

weak_ptr 弱引用,解决引用计数时互相引用形成循环引用,通常的做法是owner持有指向child的shared_ptr,child持有指向owner的weak_ptr。

fork函数

Fork:创建一个和当前进程映像一样的进程可以通过fork( )系统调用:

#include <sys/types.h>

#include <unistd.h>

pid_t fork(void);

成功调用fork( )会创建一个新的进程,它几乎与调用fork( )的进程一模一样,这两个进程都会继续运行。在子进程中,成功的fork( )调用会返回0。在父进程中fork( )返回子进程的pid。如果出现错误,fork( )返回一个负值。

最常见的fork( )用法是创建一个新的进程,然后使用exec( )载入二进制映像,替换当前进程的映像。这种情况下,派生(fork)了新的进程,而这个子进程会执行一个新的二进制可执行文件的映像。这种“派生加执行”的方式是很常见的。

在早期的Unix系统中,创建进程比较原始。当调用fork时,内核会把所有的内部数据结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容逐页的复制到子进程的地址空间中。但从内核角度来说,逐页的复制方式是十分耗时的。现代的Unix系统采取了更多的优化,例如Linux,采用了写时复制的方法,而不是对父进程空间进程整体复制。

析构函数作用

作用:在对象生命周期结束时释放对象所占用的内存空间;
如果析构函数不被声明成虚函数,则编译器采用的绑定方式是静态绑定,在删除基类指针时,只会调用基类析构函数,而不调用派生类析构函数,这样就会导致基类指针指向的派生类对象析构不完全。若是将析构函数声明为虚函数,则可以解决此问题。

隐式类型转换与explicit

隐式转换指的是不需要用户干预,编译器私下进行的类型转换行为。
C++ 隐式转换的原则:
基本数据类型的转换以低精度到高精度,即保证精度不丢失。如:char 到 int,int 到 long。
自定义对象:子类对象可隐式的转换为父类对象。
explicit作用是防止类成员函数的构造函数进行隐式转换;
由于只有当类的构造函数只有一个参数时,才可以进行隐式转换,因此,explicit对其有效。
参考文献

extern “C”

参考

主要功能:
1.C++中使用C语言代码时,extern "c"可以告诉编译器这部分代码按C语言编译,这是由于C++是支持函数重载的,而C语言是不支持的
2.被extern "C"限定的函数或变量是extern类型的

new/delete与malloc/free区别

malloc/free为C语言的标准库函数;只负责开辟空间并释放;不可重载
new/delete为C++的操作运算符,operator new()和operator delete();构造函数和析构函数负责初始化和清理。可重载

具体细节

RTTI(Run-Time Type Identification)

运行时类型识别,旨在为程序在运行阶段确定对象类型提供一种标准方式。
提供两个操作符:
typeid操作符:返回指针和引用的实际类型;<typeinfo>
dynamic_cast操作符:将基类类型的指针或引用安全地转换为派生类指针或引用。`参考

右值引用

左值可以取地址,右值不可以取地址;
右值引用只能绑定右值;
左值引用只能绑定左值;
常量左值能绑定左值和右值;
参考

虚函数表实现多态

虚函数表用于存放虚函数的地址
类对象在内存中通过虚表指针vfptr,指向虚表首地址,通过偏移量的形式访问虚表函数地址。
发生单继承时,派生类内存布局先是复制一份基类内存布局,然后自己的布局,虚表指针指向自己的虚表,基类中被派生类覆盖的函数地址失效
发生多继承时,派生类有几个父类就有几张表,按照继承顺序,先布局基类的虚表然后布局自己,直至基类排布完毕,继承来的多张表是独立的,使用首地址+偏移量访问
参考

const修饰成员函数目的

类的const数据常量只能由const成员函数调用;
表明该成员函数不修改成员变量

STL allocator空间配置器

参考

由:
内存分配: alloc::allocate();
对象构造:::construct();
对象析构:::destory();
内存释放:alloc::deallocate();

STL容器

1.序列式容器
array、vector、deque、list、slist
配接器:priority_queue 由vector结合heap算法构成
stack 和queue由deque或list构成
2.关联式容器
set、map、multiset、multimap、hashtable、hashset、hashmap

进程线程区别

进程 是操作系统资源分配的基本单位;
线程 是任务调度和执行的基本单位。

开销方面:
每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;
线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

所处环境:
在操作系统中能同时运行多个进程(程序);
在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)

内存分配:
系统在运行的时候会为每个进程分配不同的内存空间;
对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。

进程栈和线程栈的区别

参考
1.进程栈大小在进程执行时随机确定,至少大于线程栈但不超过线程栈的两倍;
线程栈大小是固定的,Linux默认线程栈大小为8MB。
2.进程栈的空间开辟在所属进程的堆区,线程栈的起始地址和大小存放在pthread_attr_t中,线程栈的大小是用来初始化避免栈溢出的缓冲区的大小,并不是判断栈是否越界。

进程间通信方式

参考

  1. 无名管道
    特点:
    1.半双工(数据在一个方向流动),读和写;
    2.只能用于父子进程或兄弟进程间通信;
    3.不属于普通文件,只存在于内存中,可通过read\write函数传递
#include<unistd.h>
int pipe(int fd[2]); //失败返回-1,成功返回0
#include<stdio.h>
#include<unistd.h>

int main()
{
    int fd[2];  // 两个文件描述符
    pid_t pid;
    char buff[20];

    if(pipe(fd) < 0)  // 创建管道
        printf("Create Pipe Error!\n");

    if((pid = fork()) < 0)  // 创建子进程
        printf("Fork Error!\n");
    else if(pid > 0)  // 父进程
    {
        close(fd[0]); // 关闭读端
        write(fd[1], "hello world\n", 12);
    }
    else
    {
        close(fd[1]); // 关闭写端
        read(fd[0], buff, 20);
        printf("%s", buff);
    }

    return 0;
}
  1. FIFO命名管道
    特点:
    1.是特殊设备文件,有路径名与之关联;
    2.可以在无关进程间通信;
#include<sys/stat.h>
int mkfifo(const char* pathname,mode_t mode);
 //失败返回-1,成功返回0
  1. 消息队列
    消息的链接表,存放于内核,一个消息队列由一个标识符来标识。
    特点:
    1.消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
    2.消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
    3.消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
#include <sys/msg.h>
// 创建或打开消息队列:成功返回队列ID,失败返回-1
int msgget(key_t key, int flag);
// 添加消息:成功返回0,失败返回-1
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// 读取消息:成功返回消息数据的长度,失败返回-1
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
// 控制消息队列:成功返回0,失败返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
  1. 信号量
    信号量(semaphore)是一个计数器,用于实现进程间的互斥与同步。
    特点:
    1.信号量用于进程间同步,需结合共享内存传递数据;
    2.信号量基于操作系统PV操作,属于原子操作;
    3.对信号量的PV操作对信号量的加减可以为任意值;
    4.支持信号量组
#include <sys/sem.h>
// 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
int semget(key_t key, int num_sems, int sem_flags);
// 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);
// 控制信号量的相关信息
int semctl(int semid, int sem_num, int cmd, ...);

struct sembuf
{
    short sem_num; // 信号量组中对应的序号,0~sem_nums-1
    short sem_op;  // 信号量值在一次操作中的改变量
    short sem_flg; // IPC_NOWAIT, SEM_UNDO
}
  1. 共享内存
    共享内存(shared memory),指两个或多个进程共享一个给定的存储区。
    特点:
    1.最快的IPC,进程对内存直接存取;
    2.多个进程同时操作,需同步;
    3.信号量用于共享内存同步;
#include <sys/shm.h>
// 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
int shmget(key_t key, size_t size, int flag);
// 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
void *shmat(int shm_id, const void *addr, int flag);
// 断开与共享内存的连接:成功返回0,失败返回-1
int shmdt(void *addr);
// 控制共享内存的相关信息:成功返回0,失败返回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);

写时复制技术(copy-on-write)

原理:在string类中,使用引用计数,当第一个类构造时,string的构造函数会根据传入的参数从堆上分配内存,当有其它类需要这块内存时,这个计数为自动累加,当有类析构时,这个计数会减一,直到最后一个类析构时,此时的RefCnt为1或是0,此时,程序才会真正的Free这块从堆上分配的内存。

虚拟内存

参考
作用:解决多个进程对内存操作时的冲突问题
在操作系统中,虚拟内存机制使每个进程都以为自己占用了全部内存,在进程访问内存时,操作系统会把进程提供的虚拟内存地址转换为物理地址,获取数据(内存管理单元MMU专门用于地址转换)
当进程访问的物理地址未分配,系统则产生缺页中断,在中断处理时,系统切到内核态为进程虚拟地址分配物理地址。
优点:

  1. 有助于内存管理,体现在:

    • 内存完整性:每个进程都认为自己的内存是连续的地址,不用考虑大块地址的分配问题;
    • 安全:由于进程访问内存时通过页表寻址,操作系统在页表的各个项目上添加各种访问权限标识位,可以实现内存的权限控制。
  2. 实现内存和数据的共享,因为不同线程的虚拟地址可以指向同一物理内存。

  3. swap分区,将暂时不用的内存数据放到磁盘上,等这些数据被需要时再将数据加载到内存。

用户态与内核态

参考
CPU有些指令非常危险,比如清内存、设置时钟等,用错会导致系统崩溃,死机。
CPU指令分为特权指令和非特权指令,特权指令只允许操作系统及其相关模块使用。
inter CPU将特权等级分为4级:Ring 0~3;
Linux中Ring 0为内核态,Ring3为用户态。
每当用户进程使用系统调用时,都自动将运行模式从用户态转为内核态,进程在内核空间运行。
用户态转内核触发手段:

  1. 系统调用:用户进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如read()、fork()等。(核心其实是系统为用户特别开放的);
  2. 中断:当外围设备完成用户请求后,向CPU发送中断信号,这时CPU会暂停执行下一条指令(用户态),转而执行该中断信号对应的中断处理程序(内核态)
  3. 异常:CPU执行用户态程序时,发生某些不可知的异常,当前程序切换到处理此异常的内核相关程序,如缺页异常。

页表的原理

参考

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

toctor

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值