C++知识点(一)

xiaoxiao2021-02-28  67

0x01. C++ 多态的实现方式,虚函数的底层实现细节

C++中的多态分为静态多态和动态多态,也称为编译时多态和运行时多态。 静态多态包括:函数重载、模板; 动态多态包括:虚函数。

C++通过虚函数实现动态多态: 在基类的成员函数前加上virtual关键字,在派生类中重写该函数,运行时根据对象的实际类型调用相应的函数。 原理: 对于每个定义了虚函数的类和它的派生类,都会产生一个虚函数表,这个虚函数表是类共享的。每个对象里都有一个虚表指针vf_ptr,指向该类型的虚函数表vf_table(虚表存放在只读数据段.rodata)。动态联编在基类指针或引用指向不同的派生类对象时发生,若派生类对象重写了虚函数,则虚表对应项将被覆盖,实际调用中根据对象的虚表指针vf_ptr找到该类的虚表,调用对应的函数。

早绑定/晚绑定、静态绑定/动态绑定: 早绑定又称静态绑定:在程序编译期发生,即编译期就确定将要调用的函数地址。 晚绑定又称动态绑定:在程序运行期发生,即在程序运行中确定要调用的函数地址。

p->Show(); # 静态绑定 0x0139B5D8 push 0Ah # 参数压栈 0x0139B5DA mov ecx,dword ptr [p] 0x0139B5DD call Base::Show (013916BDh) # 调用函数 p->Show(); # 动态绑定 0x009AA858 mov esi,esp 0x009AA85A push 0Ah # 参数压栈 0x009AA85C mov eax,dword ptr [p] # 将对象地址放入eax 0x009AA85F mov edx,dword ptr [eax] # 将对象中的虚表指针放入寄存器 0x009AA861 mov ecx,dword ptr [p] 0x009AA864 mov eax,dword ptr [edx] # 查虚表得到虚函数地址 0x009AA866 call eax # 调用相应的虚函数 0x009AA868 cmp esi,esp 0x009AA86A call __RTC_CheckEsp (09A147Eh) # 调整栈平衡

0x02. C++内存泄露/资源泄露

调用malloc/new 未free/delete 或 在执行free/delete 之前抛出异常 发生浅拷贝对象默认赋值 基类指针指向堆区资源,而基类析构函数非虚,则派生类无法析构 new Test[100] -> delete Test,使用new一次生成多个对象,而没有使用delete[],多个对象只会调用一次析构函数。 构造函数中抛出异常(new -> bad_alloc),退出时未调用析构函数,申请的内存将无法释放。 socket/fd未close(fd进程上限:2^16=65535,即epoll可操作fd上限) 产生僵尸进程,进程内核栈内存泄露(8K)

0x03. volatile关键字的作用

volatile关键字提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了,将出现不一致。

一般情况下,volatile用在如下几个地方: 1. 中断服务程序中修改的供其它程序检测的变量需要加volatile。 1. 多任务环境下,各任务间共享的标志应该加volatile。 1. 存储器映射的硬件寄存器通常要加volatile说明,因为每次对它的读写都可能有不同的意义。

多线程下的volatile(防止多线程对共享变量进行缓存): 当两个线程都要用到某个变量且该变量的值会被改变时,应该用volatile声明,防止编译器优化把变量从内存装入CPU寄存器中。如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,会造成程序的错误执行。

0x04. C++的函数重载

函数重载: 是指在同一作用域内,可以有一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数。 优点: 减少了函数名的数量,避免了名字空间的污染。 编译器实现: 应用源码编译之后,编译器对函数进行签名(作用域+返回类型+函数名+参数列表),从而重载函数的名字变了。例如

void print(int i) // 全局函数编译后函数名:_Z5printi class test{ public: void print(int i); // 编译后函数名: _ZN4test5printEi void print(char c); // 编译后函数名: _ZN4test5printEc }

函数名调用解析: 为了估计哪个重载函数最合适,需要依次按照规则来判断: 精确匹配: 参数匹配而不做转换,或者只做微小的转换,如数组名到指针、函数名到指向函数的指针。 提升匹配: 即整数提升(bool到int,char到int,short到int)。 使用标准转换匹配: 使用用户自定义匹配: 使用省略号匹配:

0x05. 指针和引用的区别

相同点:

都是地址的概念: 指针指向一块内存,内容是所指内存的地址;引用是某块内存的别名。

区别:

指针是一个实体,而引用仅是个别名;引用使用时无需解引用(*),指针需要解引用;引用只能在定义时被初始化一次,之后不可变;指针可以多次赋值;引用没有const,指针有const,const指针不可变;引用不能为空,指针可以为空;sizeof 引用 得到的是引用所指对象的大小;而sizof 指针得到的是指针本身的大小。指针和引用的自增运算意义不一样;

联系:

引用在C++语言内部用指针实现(底层汇编操作一样)对一般应用而言,把引用理解为指针,不会犯严重语义错误。引用是操作受限的指针(仅允许读取内容操作)

0x06. C++内存对齐

为什么需要内存对齐?

平台原因: 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。性能原因: 数据结构应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

内存对齐规则

对于类(结构或联合)的各个成员,第一个成员位于偏移为0的位置,以后每个数据成员的偏移量必须是 min(#pragma pack (n) 指定的数n, 该数据成员的自身长度)的整数倍数;在数据成员完成各自对齐之后,类(结构或联合)本身也要对齐,对齐按照min(#pragma pack (n) 指定的数n, 结构或联合最大数据成员长度)进行。 如果类中的成员为类,则类成员要从min(#pragma pack (n) 指定的数n, 结构或联合最大数据成员长度)开始存储。

VC、VS等编译器默认是 #pragma pack(8) (可选1,2,4,8,16),g++、clang++编译器默认是#pragma pack(4) (可选1,2,4)

0x07. 内联函数与宏

内联函数在调用的地方被展开,通过编译器控制来实现的;宏是由预处理器在预处理阶段进行替换。编译器会对内联函数的参数类型进行安全检查或自动类型转换,而宏定义则不会。内联函数在运行时可调试,而宏定义不可以。内联函数可以访问类的成员变量,而宏定义不能。

注意: 1. 递归函数不能定义为内联函数; 2. 内联函数一般适合于不存在循环等复杂的结构且只有1-5条语句的小函数上,否则编译系统将该函数视为普通函数; 3. 内联函数只能先定义后使用; 4. 对内联函数不能进行异常的接口声明。

0x08. new与malloc的区别,delete和free的区别

new是C++运算符;malloc是标准库函数,需要包含库文件。new自动计算需要分配的内存空间;malloc需要手工计算分配多大空间。new建立的是一个对象;而malloc分配的是一块内存。new会调用构造函数,而malloc不能;delete会调用析构函数,而free不能new是类型安全的,而malloc不是,比如:int* p = new float[2]; //编译时指出错误, int* p = malloc(2*sizeof(float)); //编译时无法指出错误new一般由两部构成,分别是new操作和构造。new操作对应malloc,且new操作可以重载、可以自定义内存分配策略、甚至不做内存分配。而malloc不能。new操作符从自由存储区上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,自由存储区可以是堆,也可以是静态存储区,取决于new操作符在实现细节。有专门的new[]与delete[]处理数组类型,而malloc没有。

0x09. C++中的强制类型转换

1. reinterpret_cast

仅仅重新解释类型,但没有进行二进制的转换: 1. 转换的类型必须是一个指针、引用、算术类型、函数指针或成员指针; 2. 在比特位级别上进行转换。可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针; 3. 最普通的用途就是在函数指针类型之间进行转换; 4. 很难保持移植性。

2. static_cast

类似于C风格的强制转换。无条件转换、静态类型转换。用于: 1. 基类和子类之间转换:其中子类指针转换成父类指针是安全的;但父类指针转换成子类指针是不安全的。 2. 基本数据类型转换。 3. 把空指针转换成目标类型的空指针。 4. 把任何类型的表达式转换成void类型。 5. static_cast不能去掉类型的const、volatile属性。

3. dynamic_cast

有条件转换、动态类型转换,运行时类型安全检查(转换失败返回NULL); 1. 安全的基类和子类之间转换 2. 必须要有虚函数 3. 相同基类不同子类之间的交叉转换,但结果是NULL。

4. const_cast

设置或去掉类型的const或volatile属性。

总结: 去const属性用const_cast; 基本类型转换用static_cast; 多态类之间的类型转换用dynamic_cast; 不同类型的指针类型转换用reinterpret_cast;

0x10. const修饰指针

对于指针变量有以下四种情况: 1. 指向非const对象的指针 将非const对象的指针指向一个常量对象将引起编译错误 2. 指向const对象的指针 不能通过指针修改常量的值,但是指针本身可以修改 3. const指针 声明const指针时,必须同时对其进行初始化 4. 指向const对象的const指针 声明时必须初始化,指针指向的对象以及指针本身都不能修改

0x11. typedef和#define的区别

typedef是用来声明自定义数据类型,配合各种原有数据类型来达到简化编程的目的; #define是预处理指令 1. 首先,两者指向时间不同 typedef在编译阶段有效,有类型检查的功能。 #define是宏定义,发生在预处理阶段,不进行任何检查 2. 功能不同 typedef用来定义类型的别名,类型不仅包含内部类型,还包含自定义类型。定义机器无关的类型。 #define可以为类型取别名,还可以定义常量、变量、编译开关等。

作用域不同 typedef 有自己的作用域 #define 没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用。

对指针的操作 二者修饰指针类型时,作用不同

0x12. 链接指示:extern “C”

编写C++程序有时需要调用其他语言编写的函数,比如C, Fortran等。C++使用链接指示(linkage directive)标注这种混合编程编写的函数。

声明一个非C++函数 链接指示可以有两种形式:单个或复合。链接指示不能出现在类定义或函数定义的内部。 单语句: extern "C" size_t strlen(const char*); 复合语句: extern "C"{ int strcmp(const char*, const char*); char *strcat(char*, const char*); }

链接指示与头文件 复合语句: extern "C" { #include <string.h> }

指向extern “C”函数的指针 extern "C" void (*pf)(int); 当使用pf调用函数时,编译器认定当前调用的是一个C函数。

链接指示对整个声明都有效 当使用链接指示时,不仅对函数有效,而且对作为返回类型或形参类型的函数指针也有效。

导出C++函数到其他语言 通过使用链接指示对函数进行定义,可以令一个C++函数在其他语言编写的程序中可用。 对链接到C的预处理器的支持: 有时需要在C和C++中编译同一个源文件,为了实现这一目的,在编译C++版本的程序时预处理器定义__cplusplus(两个下划线)。利用这个变量,我们可以在编译C++程序的时候有条件地包含进来一些代码:

#ifndef __cplusplus   //正确:我们在编译C++程序   extern "C" #endif int strcmp( const char*, const char* ); 重载函数与链接指示 链接指示与重载依赖于目标语言,目标语言支持则支持,C不支持重载,所以不能extern “C”用于相同函数名类型。如果在一组重载函数中有一个是C函数,其余的必然都是C++函数。

0x13. 如何定义一个只能在堆上(栈上)生成对象的类

在C++中,类的对象建立分为两种,一种是静态建立,如A a;另一种是动态建立,如A *ptr = new A;这两种方式有区别: 静态建立类对象: 是由编译器为对象在栈空间中分配内存,然后在这片栈内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数。 动态建立类对象: 使用new运算符将对象建立在堆空间中,这个过程分为两步:第一步是指针operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数,初始化这片堆内存空间。使用这种方法,间接调用类的构造函数。

1. 类对象只能建立在堆上

类对象只能建立在堆上,就不能静态建立类对象,即不能直接调用类的构造函数。 编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性以及非静态函数。如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。因此,将析构函数设为私有,类对象就无法建立在栈上。例如:

class A{ public: A(){} void destory() {delete this;} private: ~A(){} };

缺点: 1)无法解决继承问题,如果A作为其他类的基类,则析构函数通常设为virtual,然后在子类中重写,以实现多态。因此,析构函数不能设为private。可以将析构函数设为protected。 2)类的使用很不方便,使用new建立对象,却使用delete释放对象,而不是使用delete。为了统一,可以将构造函数设为protected,然后提供一个public的静态函数来完成构造,这样不使用new。

class A{ protected: A() {} ~A() {} public: static A* create(){ return new A(); } void destory(){ delete this; } }; int main(int argc, char* argv[]){ A *aptr = A::create(); return 0; }

2. 类对象只能建立在栈上

只有使用new运算符,对象才会建立在堆上。因此,只要禁用new运算符就可以实现类对象只能建立在栈上。将operator new()设为私有即可。

class A{ private: void *operator new(size_t) {} void operator delete(void *ptr) {} public: A(){} ~A(){} };

0x14. 必须在构造函数初始化式里进行初始化的数据成员

const常量:常量只能初始化,不能赋值引用类型:只能在定义时初始化,并且不能被重新赋值没有默认构造函数的类类型:初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数。

static 对象属于类,不属于具体的对象; static const对象不能在初始化列表中初始化。

0x15. 类的构造函数和析构函数不要调用虚函数

构造函数不要调用虚函数

基类构造函数是在派生类之前执行的,在基类构造函数运行的时候对象的派生类的数据成员部分还没有初始化。如果在基类的构造过程中,对虚函数的调用传递到派生类,派生类对象可以参照引用局部的数据成员。但是数据成员此时未初始化,这会导致无休止的未定义行为。

析构函数不要调用虚函数

如果派生类的对象进行析构,首先调用派生类的析构函数,然后在调用基类的析构时,遇到一个虚函数,由两种选择:1)调用虚函数的基类版本,那么虚函数则失去了运行时调用正确版本的意义;2)调用虚函数的派生类版本,此时对象的派生类部分已经完成析构,函数调用会导致未定义行为。 实际情况使用基类版本,如果虚函数的基类版本不是纯虚实现,不会有严重错误发生。

0x16. sizeof和strlen区别

sizeof操作符的结果类型size_t,在头文件中typedef为unsigned int类型;sizeof是算符,strlen是函数;sizeof可以用类型、函数做参数;strlen只能是char* 做参数,且必须是以”\0”结尾的;数据做sizeof的参数不退化,传递给strlen就退化为指针;大部分编译程序,在编译的时候就把sizeof计算过了,strlen的结果在运行的时候才能计算出来
转载请注明原文地址: https://www.6miu.com/read-60231.html

最新回复(0)