C++易混淆知识点总结

1. struct 和 union 的区别

  • 结构体struct:把不同类型的数据组合成一个整体,自定义类型。
  • 共同体union:使几个不同类型的变量共同占用一段内存

2. 字节对齐

许多计算机系统对基本数据类型合法地址做出了一些限制,要求某种类型对象的地址必须是某个值K(通常是2,4或8)的倍数。为了方便快速地寻址,编译器会采用字节对齐,将下一个变量地址放置在系统能快速读取的位置(如:32 位系统,放在偶地址的变量能够 1 个读周期取到值,而放在奇地址的变量却需要 2 个读周期才能取到值,故会存在字节对齐)。

关于内存对齐,有四个重要的基本概念:

  • 数据类型自身的对齐值:
    对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,单位字节。
  • 结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。
  • 指定对齐值:#pragma pack(n),n=1,2,4,8,16改变系统的对齐系数
  • 数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值。

3. 字节序(小正逆大)

  • 小端模式:Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
  • 大端模式:Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
    网络字节顺序(大端)是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian排序方式。

4. static的作用

4.1. 对普通变量的作用

  • static修饰局部变量

它改变了局部变量的存储位置(从原来的栈中存放改为静态存储区)及其生命周期(局部静态变量在离开作用域之后,并没有被销毁,而是仍然驻留在内存当中,直到程序结束,只不过我们不能再对他进行访问),但未改变其作用域。

  • static修饰全局变量。

并未改变其存储位置及生命周期,而是改变了其作用域,使当前文件外的源文件无法访问该变量,好处如下:(1)不会被其他文件所访问修改(2)其他文件中可以使用相同名字的变量,不会发生冲突。

4.2. 对成员变量的作用

用static修饰类的数据成员成为类的全局变量,会被类的所有对象共享,包括派生类的对象。因此,static成员必须在类外进行初始化(初始化格式: int base::var=10; ),而不能在构造函数内进行初始化,不过也可以用const修饰static数据成员在类内初始化。

4.3. 对成员函数的作用

用static修饰成员函数,使这个类只存在这一份函数,所有对象共享该函数,不含this指针。
静态成员是可以独立访问的,也就是说,无须创建任何对象实例就可以访问。base::func(5,3);当static成员函数在类外定义时不需要加static修饰符。
在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员。因为静态成员函数不含this指针。

5. const的作用

  • 限定变量为不可修改。
  • 限定成员函数不可以修改任何数据成员。
  • const与指针:
    • const char *p,不能改变指向的内容;
    • char * const p,就是将P声明为常指针,它的地址不能改变,是固定的,但是它的内容可以改变。

6. 不能同时使用const和static修饰类的成员函数

static的作用是表示该函数只作用在类型的静态变量上,与类的实例没有关系;而const的作用是确保函数不能修改类的实例的状态,与类型的静态变量没有关系。因此不能同时用它们。

7. 指针常量和常量指针

  • 指针常量(char * const p):指针变量的值一经初始化(初始化是必要的),不可以改变指向另一个变量(但可以改变已经指向的变量的内容)。
  • 常量指针(const char *p):指向常量的指针。不可以改变指向某变量的值,可以改变指向另一个变量。

8. 指针和引用的区别

  • 指针是一个新的变量,只是这个变量存储的是另一个变量的地址,我们通过访问这个地址来修改变量。
  • 引用只是一个别名,还是变量本身。对引用进行的任何操作就是对变量本身进行操作,因此以达到修改变量的目的。
  • 引用在定义的时候必须初始化;指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。
  • 指针和引用的自增(++)运算意义不一样。

9. 多态及其用途

  • 定义:“一个接口,多种方法”,程序在运行时才决定调用的函数。
  • 实现:C++多态性主要是通过虚函数实现的,虚函数允许子类重写override(注意和overload的区别,overload是重载,是允许同名函数的表现,这些函数参数列表/类型不同)。
  • 目的:接口重用。封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。
  • 用法:声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。
  • 详见:C++ 中的四种多态

10. 重载、覆盖与重写的区别

10.1. Overload(重载)

在C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,但参数或返回值不同(包括类型、顺序不同),即函数重载。

  • 相同的范围(在同一个类中);
  • 函数名字相同;
  • 参数不同;
  • virtual 关键字可有可无。

10.2. Override(覆盖)

是指派生类函数覆盖基类函数,特征是:

  • 不同的范围(分别位于派生类与基类);
  • 函数名字相同;
  • 参数相同;
  • 基类函数必须有virtual 关键字。

注:重写基类虚函数的时候,会自动转换这个函数为virtual函数,不管有没有加virtual,因此重写的时候不加virtual也是可以的,不过为了易读性,还是加上比较好。

10.3. Overwrite(重写)

是指派生类的函数屏蔽了与其同名的基类函数,规则如下:

  • 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
  • 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

11. 面向对象的三要素

封装、继承、多态

12. 纯虚函数与抽象类

将函数定义为纯虚函数virtual ReturnType Function() = 0;,纯虚函数不能再在基类中实现,编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。
特点:

  • 在基类中抽象出一个方法,且该基类只做能被继承,而不能被实例化;(避免类被实例化且在编译时候被发现,可以采用此方法)
  • 这个方法必须在派生类(derived class)中被实现;目的:使派生类仅仅只是继承函数的接口。
  • 抽象类只能作为基类来使用,而继承了抽象类的派生类如果没有实现纯虚函数,而只是继承纯虚函数,那么该类仍旧是一个抽象类,如果实现了纯虚函数,就不再是抽象类。

13. 虚函数的作用

实现动态绑定,即运行期绑定

14. 析构函数定义为虚函数的原因

基类指针可以指向派生类的对象(多态性),如果删除该指针delete p;就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。

如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。所以,将析构函数声明为虚函数是十分必要的。

构造函数为什么不能为虚函数(延伸)
虚函数对应一个指向虚函数表的指针,而这个指向vtable的指针是存储在对象的内存空间的。假设构造函数是虚的,就需要要通过查询vtable来调用,但是对象还没有实例化,因此也就不存在vtable,所以构造函数不能是虚函数。

15. 深拷贝与浅拷贝:

  • 浅拷贝,默认的拷贝构造函数只是完成了对象之间的位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存(并未另申请内存)。这就会导致野指针问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
  • 深拷贝,自定义复制构造函数需要注意,对象之间发生复制,资源重新分配,即A有5个空间,B也应该有5个空间,而不是指向A的5个空间。

16. vector中size()和capacity()的区别。

  • size()指容器当前拥有的元素个数;
  • capacity()指容器在必须分配存储空间之前可以存储的元素总数。

17. map和set默认是排序的

map和set的底层实现主要是由红黑树实现的

18. 在TCP连接建立过程中,若client发送了SYN消息后,client、server可能状态

  • client处于SYN_SENT状态;
  • server可能仍处于listen状态(未收到SYN消息),或处于SYN_RCVD状态

19. 析构函数不推荐抛出异常

从语法上面讲,析构函数抛出异常是可以的,C++并没有禁止析构函数引发异常,但是C++不推荐这一做法,从析构函数中抛出异常是及其危险的。

析构函数可能在对象正常结束生命周期时调用,也可能在有异常发生时从函数堆栈清理时调用。前一种情况抛出异常不会有无法预料的结果,可以正常捕获;但后一种情况下,因为函数发生了异常而导致函数的局部变量的析构函数被调用,析构函数又抛出异常,本来局部对象抛出的异常应该是由它所在的函数负责捕获的,现在函数既然已经发生了异常,必定不能捕获,因此,异常处理机制只能调用terminate()。

20. 进程和线程

20.1. 进程

是并发执行的程序在执行过程中分配和管理资源的基本单位,每一个进程都有一个自己的地址空间,即进程空间或(虚空间)。进程至少有 5 种基本状态,它们是:初始态,执行态,等待状态,就绪状态,终止状态。

20.2. 线程

是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。

20.3. 进程和线程的区别

进程和线程都是由操作系统所体会的程序运行的基本单元,系统利用该基本单元实现系统对应用的并发性。进程和线程的区别在于:

  • 地址空间:线程是进程内的一个执行单元;一个程序至少有一个进程,一个进程至少有一个线程;它们共享进程的地址空间;而各个进程有自己独立的地址空间;
  • 资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源
  • 划分尺度:线程的划分尺度小于进程,使得多线程程序的并发性高(进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。)
  • 执行过程:每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  • 线程是处理器调度的基本单位,而进程不是
  • 管理与分配:多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

21. 进程间通信方式

  • 管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
  • 命名管道(FIFO):有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  • 消息队列(MessageQueue):消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  • 共享内存(SharedMemory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
  • 信号量(Semaphore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  • 套接字(Socket):套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
  • 信号(sinal): 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。