C++类型转换

  1. const_cast:用于将const变量变为非const
  2. static_cast:用于各种隐式转换,比如非const转const,void*转指针等,static_cast能用于多态向上转化(派生类转基类),如果向下转(基类转派生类)能成功但是不安全并且结果未知
  3. dynamic_cast:用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用,向下转换(基类转派生类)时,如果是非法的对于指针返回NULL,对于引用抛异常。它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否进行向下转换。
  4. reinterpret_cast:几乎什么都可以转,比如将int转指针,本质上依赖机器可能会出问题,尽量少用。
  5. 用C++这四种而不用C的强制类型转换,主要是因为C的强制类型转换表面上看起来功能强大什么都能转但是转换不够明确,不能进行错误检查,容易出错。

C/C++中指针和引用的区别

  1. 指针有自己的一块空间,而引用只是别名
  2. 使用sizeof可知32位机器中一个指针的大小为4字节,而引用则是被引用对象的大小
  3. 指针可以被初始化为nullptr空指针,但是引用必须被初始化且必须是一个已有对象的引用
  4. 作为参数传递,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会直接改变引用所指向的对象
  5. 可以有const指针,但是没有const引用
  6. 指针在使用中可以指向其他对象,但是引用只能是一个对象的引用,不能被改变
  7. 指针可以有多级指针(**p),而引用只有一级
  8. 指针和引用使用++运算符的意义不一样
  9. 如果返回动态内存分配的对象或者内存必须使用指针,引用可能引起内存泄漏

C++中的智能指针

  1. C++11后有四个智能指针:auto_ptr、shared_ptr、weak_ptr、unique_ptr,其中C++11之后将auto_ptr弃用。智能指针的作用在于管理裸指针,因为使用裸指针申请动态内存可能会出现申请的空间在函数结束时忘记释放,造成内存泄露的情况。使用智能指针由于智能指针本质上是类,当超出了智能指针类的作用域时会自动调用其析构函数自动释放资源。
  2. auto_ptr拥有独占性资源但是可以将其资源赋给其他auto_ptr编译器不会报错,因此存在潜在的内存崩溃问题,而后续的unique_ptr弥补了这一问题
  3. unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象,对于避免资源泄露特别有用,并且由于其复制构造函数和复制赋值函数均为delete函数因此不能用一个unique_ptr来初始化另一个unique_ptr或者赋给另外一个(临时右值unqiue_ptr除外)。
     int x=1;
     unqiue_ptr<int> p1=make_unique(x);
     auto p2=p1;                      //报错
     unqiue_ptr<int> p3(new int(1));  //不报错
    
  4. 使用std::move移动操作可以把一个裸指针的所有权从一个unqiue_ptr转移到另一个unqiue_ptr。
  5. shared_ptr实现共享式拥有概念,多个shared_ptr可以指向同一对象,该对象和相关资源会在“最后一个引用被销毁”的时候释放,使用计数机制来表明资源被几个指针共享,可以通过成员函数use_count()来查看计数。shared_ptr除了通过new来构造,还可以通过传入unique_ptr、weak_ptr来构造,调用reset当前指针会释放资源所有权,计数减一,当计数==0时资源会被释放。
  6. weak_ptr不控制对象生命周期,指向一个shared_ptr管理的对象,只是提供了对管理对象的一个访问手段。weak_ptr设计的目的是配合shared_ptr而引用的一种智能指针来协助weak_ptr工作,只可以从一个shared_ptr或者另一个weak_ptr对象构造。weak_ptr的创建和销毁都不会改变引用计数。weak_ptr是用来解决shared_ptr相会引用时的死锁问题,因为两个shared_ptr如果相互引用则这两个指针的引用计数永远不会下降为0,资源永不释放。
  7. 我们不能直接通过weak_ptr直接访问对象,而是得调用lock()成员函数将weak_ptr转换为shared_ptr然后再访问。
  8. C/C++中的野指针就是指向一个已删除的对象或者未申请访问受限内存区域的指针。
  9. 智能指针也存在内存泄露的情况,当两个对象互相使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。解决方法就是使用weak_ptr来指向对方。

类成员函数及函数指针相关问题

  1. 基类的析构函数设置成虚函数的原因是将可能会被继承的父类中的析构函数设置成虚函数,可以保证我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
  2. C++默认的析构函数不设置成虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时才设置为虚函数。
  3. 函数指针是指向函数的指针变量,C/C++在编译时每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址,有了指向函数的指针变量后可用该指针调用函数,就如同用指针变量可引用其他类型变量一样。
  4. 函数指针可用于调用函数和做函数的参数,比如回调函数。
  5. 析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕,系统会自动执行析构函数。析构函数不能带任何参数,也没有返回值(包括void类型)。只能有一个析构函数,不能重载。
  6. 如果我们没有定义一个析构函数,编译器会自动生成一个缺省的析构函数。如果类中有指针,且在使用过程中动态申请了内存,那么最好显式构造析构函数在销毁类之前,释放掉申请的内存空间,避免内存泄漏。
  7. 类析构顺序:1)派生类本身的析构函数;2)对象成员析构函数;3)基类析构函数

fork、vfork、wait、exec函数

  1. fork函数包含在unistd.h头文件中,成功调用fork()会创建一个新的进程,它几乎与调用fork的进程一模一样,这两个进程都会继续运行。在子进程中成功的fork调用会返回0.在父进程中fork返回子进程的pid。如果出现错误,fork返回一个负值。
  2. 最常见的fork用法是创建一个新的进程,然后使用exec()载入二进制映像,替换当前进程的映像。这种情况下fork了新的进程,而这个子进程会执行一个新的二进制可执行文件的映像。
  3. 早期unix系统中,调用fork时内核会把所有的内部数据结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容逐页的复制到子进程的地址空间中,但是这样的逐页复制的复制方式是十分耗时的,而现代unix采取了更多优化比如linux,采用了写时复制的方法,而不是对父进程空间进程整体复制。
  4. 在实现写时复制之前,unix设计者就一直很关注fork后立刻执行exec所造成的地址空间的浪费,因此引入了vfork系统调用。除了子进程必须要立刻执行一次对exec的系统调用,或者调用_exit()退出,对vfork()的成功调用所产生的结果和fork是一样的。vfork会挂起父进程直到子进程终止或者运行了一个新的可执行文件的映像。通过这样的方式,vfork()避免了地址空间的按页复制。在这个过程中,父进程和子进程共享相同的地址空间和页表项。实际上vfork()只完成了一件事:复制内部的内核数据结构。因此子进程也就不能修改地址空间中的任何内存。
  5. linxu采用了写时复制的方法,以减少fork时对父进程空间进程整体带来的开销。写时复制是一种采取了惰性优化方法来避免复制时的系统开销。它的前提很简单:如果有多个进程要读取它们自己的资源的副本,那么复制是不必要的,每个进程只要保存一个指向该资源的指针就行了。如果一个进程要修改自己的那份资源“副本”,那么就会复制那份资源,并把复制的那份提供给进程,因此这就是在写入时进行复制。
  6. 写时复制的主要好处在于:如果进程从来就不需要修改资源,则不需要进行复制。惰性算法的好处在于它们尽量推迟代价高昂的操作,知道必要的时刻才会去执行。
  7. 在使用虚拟内存的情况下,写时复制是以页为基础进行的,所以只要进程不修改它全部的地址空间,那么就不必复制整个地址空间。在fork调用结束后,父进程和子进程都相信它们有一个自己的地址空间,但实际上它们共享父进程的原始页,接下来这些页又可以被其他的父进程或子进程共享。
  8. 写时复制在内核中的实现非常简单,与内核页相关的数据结构可以被标记为只读和写时复制。如果有进程试图修改一个页,就会产生一个缺页中断。内核处理缺页中断的方式就是对该页进行一次透明复制。这时会清除页面的cow属性,表示着它不再被共享。现代的操作系统结构中都在内存管理单元(MMU)提供了硬件级别的写时复制支持,所以实现是很容易的。
  9. fork和vfork的区别:
    (1)fork的子进程拷贝父进程的数据段和代码段;vfork的子进程与父进程共享数据段
    (2)fork的父子进程的执行顺序不确定;vfork保证子进程先运行,在调用exec或exit之前与父进程数据是共享的,在它调用exec或exit之后父进程才可能被调度运行。
    (3)vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。
    (4)vfork出的子进程当需要改变共享数据段中的变量的值则拷贝子进程。
  10. 调用了wait的父进程将会发生阻塞,直到有子进程状态改变,执行成功返回0,错误返回-1。

函数重载与动态绑定

  1. 静态函数在编译时就已经确定运行时机,虚函数在运行时动态绑定。虚函数因为用了虚函数表机制调用时会增加一次内存开销。
  2. 重载:两个函数名相同,但是参数列表不同(个数,类型),返回值类型没有要求,在同一作用域中
    重写:子类继承父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是重写。
  3. 多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译时就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。在父类中声明为加了virtual关键字的函数,在子类中重写时不需要加上virtual关键字,但是建议加上override关键字。
  4. 虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表指针,这个指针指向一个虚函数表,表中放了虚函数表的地址,实际的虚函数在代码段中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数会增加内存开销,降低效率。
  5. const修饰的成员函数表明函数调用不会对对象做出任何更改,事实上,如果确认不会对对象做更改,就应该为函数加上const限定,这样无论const对象还是普通对象都可以调用该函数。const函数和其对应的非const版本相当于函数的重载。
  6. 对于内置类型,低精度的变量给高精度的变量赋值会发生隐式类型转换,其次对于只存在单个参数的构造函数的对象构造来说,函数调用可以直接使用该参数传入,编译器会自动调用其构造函数生成临时对象。
  7. C/C++中每一个函数调用都会分配函数栈,在栈内进行函数执行过程。调用前,先把返回地址压栈,然后把当前函数的esp指针压栈。函数参数的压栈顺序是从右到左。对于函数返回值,C/C++会生成一个临时变量把它的引用作为函数参数传入函数内。
  8. C++中拷贝构造函数的形参必须是引用,而不能是值传递,因为如果是值传递的话,调用拷贝构造函数的时候首先要将实参传递给形参,这个传递过程又要使用拷贝构造函数,如此循环栈将会被填满,也无法完成拷贝。

C/C++中结构体字节对齐

  1. 字节对齐的作用和原因:
    (1)各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问 一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对 数据存放进行对齐,会在存取效率上带来损失。
    (2)结构体只是一些数据的集合,它本身什么都没有。我们所谓的结构体地址,其实就是结构体第一个元素的地址。这样,如果结构体各个元素之间不存在内存对齐问题,他们都挨着排放的。对于32位机,32位编译器(这是目前常见的环境,其他环境也会有内存对齐问题),就很可能操作一个问题,就是当你想要去访问结构体中的一个数据的时候,需要你操作两次数据总线(每根数据总线每次可以读取一位bit,32位机器有32根,可以读取32位bit,64位则是可以读取64位),因为这个数据卡在中间。
  2. 结构体内存对齐的规则(未指定#pragma pack(n)时):
    (1)第一个成员起始于0偏移处;
    (2)每个成员按其类型大小和指定对齐参数n中较小的一个进行对齐;
    (3)结构体总长度必须为所有对齐参数的整数倍;
    (4)对于数组,可以拆开看做n个数组元素。
  3. 如果有#pragma pack宏,对齐方式按照宏的定义来。比如上面的结构体前加#pragma pack(1),内存的布局就会完全改变。有了#pragma pack(1),按1字节对齐,这就是理想中的没有内存对齐的世界。