目录
- C 篇
-
- 1. C 和 C
-
- 1.1 struct 和 class 区别
- 2. 对象
-
- 2.1 什么是面向对象?
- 2.2 为什么构造函数和分析函数可以是虚拟函数?
- 2.3 复制构造函数如果用值传递会有什么影响?
- 2.4 如何限制一个类对象只能在堆(堆)上分配空间?
- 2.5 public protected private
- 2.6 有哪些类别的结构方法?
- 2.7 拷贝构造函数参数中为什么有时候要加const
- 2.8 引用常量左值
- 3. 多态
-
- 3.1 什么是多态?
- 3.2 继承与多态区别与联系?
- 3.3 虚函数能内联吗?
- 4. 内存管理
-
- 4.1 new 和malloc 的区别
- 4.2 C 的内存分配
- 4.3 简述c、C 程序编译的内存分配
- 5. 关键字
-
- 5.1 extern 和 static 区别,什么情况使用前者,什么情况使用后者
- 5.2 声明和定义的区别
- 5.3 引用会占用内存空间吗?
- 5.4 strcpy和memcpy区别,现场要求手撕代码
- 5.5 虚函数能否定义模板?
- 6. 运算操作符
-
- 6.1 x=x 1,x =1,x 哪个效率高
- C
-
- 1.内存的编译
-
- 1.1 C 编程过程
- 1.2 内存管理
- 1.3 栈与堆的区别
- 1.4 变量的区别
- 1.5 头文件中全局变量定义存在哪些问题?
- 1.6 对象的创建仅限于堆叠或堆叠
- 1.7 内存对齐
- 1.8 类的大小
- 1.9 什么是内存泄漏?
- 1.10 什么是智能指针?实现智能指针的原理?
- 1.11 一个 unique_ptr 如何赋值另一个? unique_ptr 对象?
- 1.12 使用智能指针会出现什么问题?如何解决?
- 2. 语言对比
-
- 2.1 C 11 新特性
- 2.2 C和C 的区别
- 3. 面向对象
-
- 3.1 什么是面向对象?
- 3.2 重载、重写和隐藏的区别
- 3.3 什么是多态?如何实现多态?
- 4. 关键字库函数
-
- 4.1 sizeof 和 strlen 的区别
- 4.2 lambda 具体应用和使用表达式(匿名函数)的场景
- 4.3 explicit (如何避免编译器转换隐式类型)
- 4.4 static 的作用
- 4.5 static类别中使用的注意事项(定义、初始化和使用)***
- 4.6 static 全局变量与普通全局变量的异同
- 4.7 const 作用及用法
- 4.8 define 和 const 的区别
- 4.9 define 和 typedef 的区别
- 4.10 用宏实现比较大小和两个数中最小值
- 4.11 inline 功能及使用方法
- 4.12 inline 函数工作原理
- 4.13 宏定义(define)和内联函数(inline)的区别
- 4.14 new 的作用?
- 4.15 new 和 malloc 如何判断是否申请内存?
- 4.16 delete 实现原理?delete 和 delete[] 的区别?
- 4.17 new 和 malloc 的区别,delete 和 free 的区别
- 4.18 malloc 的原理?malloc 底层实现?
- 4.19 C 和 C struct 的区别?
- 4.20 为什么有了 class 还保留 struct?
- 4.21 struct 和 union 的区别
- 4.22 class 和 struct 的异同
- 4.23 volatile 功能?是否有原子,对编译器有什么影响?
- 4.24 必须在什么情况下使用 volatile, 能否和 const 一起使用?
- 4.25 extern C 的作用?
- 4.26 sizeof(1==1) 在 C 和 C 中分别是什么结果?
- 4.27 memcpy 函数的底层原理?
- 4.28 strcpy 函数有什么缺陷?
- 4.29 auto 类型推导的原理
- 5. 类相关
-
- 5.1 什么是虚函数?什么是纯虚函数?
- 5.1 虚函数和纯虚函数的区别?
- 5.2 虚拟函数的实现机制
- 5.3 虚函数表结构单继承和多继承
- 5.4 如何禁止使用构造函数?
- 5.5 默认构造函数是什么?
- 5.6 构造函数和分析函数是否需要定义为虚拟函数?为什么?
- 5.7 如何避免复制?
- 5.8 如何降低构造函数开销?
- 5.9 多重继承会发生什么?如何解决?
- 5.10 空类占多少字节?C 哪些函数会自动生成空类?
- 5.11 为何必须引用复制构造函数?
- 5.12 C 类对象的初始化顺序
- 5.13 如何禁止一类实例化?
- 5.14 为什么会更快地使用成员初始化列表?
- 5.15 实例化一个对象需要哪些阶段?
- 5.16 友元函数的作用及场景的使用
- 5.17 如何实现静态绑定和动态绑定?
- 5.18 深拷贝和浅拷贝的区别 ***
- 5.19 编译时多态和运行时多态的区别
- 5.20 要实现一个类成员函数,不允许修改类成员变量?
- 5.21 如何让类不能继承?
- 6. 语言特征相关
-
- 6.1 左值和右值的区别?如何将左值转换为右值?
- 6.2 std::move() 函数的实现原理
- 6.3 什么是指针?指针的大小和用法?
- 6.4 野指针和悬空指针是什么?
- 6.5 C 11 nullptr 比 NULL 优势
- 6.6 指针和引用的区别?
- 6.7 常量指针和指针常量的区别
- 68 函数指针和指针函数的区别
- 6.9 强制类型转换***
- 6.10 如何判断结构体是否相等?能否用 memcmp 函数判断结构体相等?
- 6.11 参数传递时,值传递、引用传递、指针传递的区别?
- 6.12 什么是模板?如何实现?
- 6.13 函数模板和类模板的区别?
- 6.14 什么是可变参数模板?
- 6.15 什么是模板特化?为什么特化?
- 6.16 include " " 和 <> 的区别
- 6.17 迭代器的作用?
- 6.18 泛型编程如何实现?
- 多线程交替打印奇偶数***
- 单例模式例程***
C++篇
1. C 和 C++
C++在C的基础上添加类,C是一种结构化语言,它的重点在于数据结构和算法。C语言的设计首要考虑的是如何通过一个过程,对输入进行运算处理得到输出,而对C++,首先要考虑的是如何构造一个对象,通过封装一下行为和属性,通过一些操作将对象的状态信息输出。
1.1 struct 和 class 区别
1)struct的成员默认是公有的,而类的程园默认是私有的; 2)C中的struct不能包含成员函数,C++中的class可以包含成员函数。
2. 对象
2.1 什么是面向对象?
就是一种对现实世界的理解和抽象,将问题转换成对象进行解决需求处理的思想。
2.2 构造函数和析构函数可不可以为虚函数,为什么?
1)构造函数不可以是虚函数,如果构造函数是虚函数,那么就需要通过vtable 来调用,但此时面对一块 raw memeory,到哪里去找 vtable 呢?毕竟,vtable 是在构造函数中才初始化的啊,而不是在其之前。因此构造函数不能为虚函数。
构造对象的时候,必须知道对象的实际类型。而虚函数行为是在运行期间确定实际类型的,在构造对象的时,对象还没有构造成功,编译器无法知道对象的实际类型是该类本身还是其派生类。
2)析构函数可以为虚函数,因为当基类的指针指向派生类对象的时候,发生多态,如果不将基类的析构函数定义为虚函数的话,那么派生类的析构函数就无法执行。
2.3 拷贝构造函数如果用值传递会有什么影响?
如果把拷贝构造函数的参数设置为值传递,那么参数肯定就是本类的一个object,采用值传递,在形参和实参相结合的时候,是要调用本类的拷贝构造函数,是不是就是一个死循环了?为了避免拷贝构造函数无限制的递归下去。
2.4 如何限制一个类对象只能在堆(栈)上分配空间
1)在堆上进行构建类对象的时候,是使用new的方法在堆区进行开辟空间。编译器管理了对象的整个生命周期,如果编译器无法调用类的析构函数会怎么样呢?这个类对象就一直占用着空间,得不到释放。比如,将类的析构函数设为私有的,那么编译器就无法调用类的析构函数来释放内存。所以编译器在为类对象分配栈空间的时候,会首先检查类的析构函数的访问性,不光是析构函数,只要是非静态的函数,编译器都会检查。如果类的析构函数是私有的,则编译器就不会在栈上为类对象分配内存了。
2)这种方法有种缺点,无法解决继承问题,因为派生类是无法通过基类的析构函数进行释放自己的。但是派生类可以访问基类的protected,可以将析构函数和构造函数用protected形式,然后提供一个public的static的函数完成构造
class A
{
protected:
A(){}
~A(){}
public:
static A* create()
{
return new A();
}
void destory()
{
delete this;
}
};
这样,调用create()函数在堆上创建类A对象,调用destory()函数释放内存。
3)只能建立在栈上
只有使用new运算符,对象才会建立在堆上,因此,只要禁用new运算符就可以实现类对象只能建立在栈上。将operator new()设为私有即可。代码如下:
class A
{
private:
void* operator new(size_t t){} // 注意函数的第一个参数和返回值都是固定的
void operator delete(void* ptr){} // 重载了new就需要重载delete
public:
A(){}
~A(){}
};
2.5 public protected private
第一: private,public,protected的访问范围:
private: 只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问. protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问 public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问 注:友元函数包括两种:设为友元的全局函数,设为友元类中的成员函数
第二:类的继承后方法属性变化:
使用private继承,父类的所有方法在子类中变为private; 使用protected继承,父类的protected和public方法在子类中变为protected,private方法不变; 使用public继承,父类中的方法属性不发生改变;
2.6 类都有哪几种构造方式?
- 默认构造函数 Student();//没有参数
- 有参构造函数 Student(int num,int age);//有参数
- 拷贝构造函数 Student(Student&);//形参是本类对象的引用
- 转换构造函数 Student(int r) ;//形参时其他类型变量,且只有一个形参
2.7 拷贝构造函数参数中为什么有时候要加const
这是因为当参数为一个临时对象的时候,临时对象是一个右值。而拷贝构造函数的参数中,如果不加const,那么就是一个非常量左值引用(非常量左值是不能引用右值的),加了const之后就是一个常量左值引用,可以引用右值。
class test{
public:
test(const test& a)
{
cout<<"拷贝构造函数"<<endl;
}
};
test get_test()
{
test a;
return a;
}
int main()
{
test b=get_test();
}
2.8 常量左值引用
常量左值引用是一个“万能”的引用类型,可以接受左值,右值,常量左值、常量右值。需要注意的是普通的左值引用是不能接受右值的。
3. 多态
3.1 什么是多态?
1)派生类对象的地址可以赋值给基类指针。对于通过基类指针调用基类和派生类中都有的同名、同参数表的虚函数的语句,编译时并不确定要执行的是基类还是派生类的虚函数;而当程序运行到该语句时,如果基类指针指向的是一个基类对象,则基类的虚函数被调用,如果基类指针指向的是一个派生类对象,则派生类的虚函数被调用。这种机制就叫作“多态(polymorphism)”。
2)静态多态(编译阶段,地址早绑定)
- 函数重载:包括普通函数的重载和成员函数的重载
- 函数模板的使用:通过将类型作为参数,传递给模板,可使编译器生成该类型的函数。
3)动态多态(运行阶段,地址晚绑定)在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。
-
派生类
-
虚函数
3.2 继承和多态区别与联系?
区别:继承是子类使用父类的方法,而多态则是父类使用子类的方法。
1) 什么是继承,继承的特点? 子类继承父类的特征和行为,使得子类具有父类的各种属性和方法。
2) 什么是多态? 相同的事物,调用其相同的方法,参数也相同时,但表现的行为却不同。
3)继承是为了重用代码,有效实现代码重用,减少重复代码的出现。
4)多态是为了接口重用,增强接口的扩展性。
3.3 虚函数可以内联吗?
当呈现非多态的时候,虚函数可以内联。因为内敛函数是在编译的时候确定函数的执行位置的, 当函数呈现多态的时候,在编译的时候不知道是将基类的函数地址,还是派生类的地址写入虚函数表中,所以当非多态的时候就会将基类的虚函数地址直接写入虚函数表中,然后通过内联将代码地址写入。
4. 内存管理
4.1 new 和malloc 的区别
1)都可用来申请动态内存和释放内存,都是在堆(heap)上进行动态的内存操作。
2)malloc和free是c语言的标准库函数,new/delete是C++的运算符。
3)new会自动调用对象的构造函数,delete 会调用对象的析构函数, 而malloc返回的都是void指针。
4)对于非内部数据类型的对象而言,光用malloc和free无法满足动态对象的要求。
5)因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
4.2 C++的内存分配
在C++中,内存分为5个区,他们分别是:
- 堆区:一般由程序员自动分配,如果程序员没有释放,程序结束时可能有OS回收。其分配类似于链表。
- 栈区:由编译器自动分配和释放,存放为运行函数分配的局部变量,函数参数,返回数据,返回地址等,其操作类似于数据结构总的栈。
- 全局区(静态区static):存放全局变量,静态变量,常量。结束后由系统释放。
- 常量区(文字常量区):存放常量字符串,程序结束后有系统释放。
- 代码区:存放函数体(类成员函数和全局区)的二进制代码。
4.3 简述c、C++程序编译的内存分配情况
- : 内存在程序 编译 时 就已 经 分配 好,这块内 存在 程序 的整 个运行 期间 都存在 。速 度快、不容易出错 , 因 为 有系 统 会善 后。例 如全 局变 量, sta tic 变量, 常量 字符 串等。
- : 在执行函数时, 函数内局部变量的存储单元都在栈上创建,函数执行结束时这些存储单元自动被释放。 栈内存分配运算内置于处理器的指令集中, 效率很高, 但是 分配的内存容量有限 。大小为2M。
- : 即动态内存分配。程序在运行的时候用 malloc 或 new 申请任意大小的内存,程序员自己负责在何时用 free 或delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活。如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏 ,另外频繁地分配和释放不同大小的堆空间将会产生堆内碎块 。
5. 关键字
5.1 extern 和 static 的区别,什么情况用前者什么情况用后者
1)extern外部变量:它属于变量声明,extern int a和int a的区别就是,前者告诉编译器,有一个int类型的变量a定义在其他地方,如果有调用请去其他文件中查找定义。
2)static静态变量:简单说就是在函数等调用结束后,该变量也不会被释放,保存的值还保留。即它的生存期是永久的,直到程序运行结束,系统才会释放,但也无需手动释放。
5.2 声明和定义的区别
1)int a 定义变量需要为变量在内存中分配存储空间
2)extern int a声明不需要分配存储空间
3)声明的目的是为了在定义之前使用,如果不需要在定义之前使用,那么就没有单独声明的必要
5.3 引用会占用内存空间吗?
引用类型的变量会占用内存空间,占用的内存空间的大小和指针类型的大小是相同的。
5.4 strcpy和memcpy的区别,现场要求手撕代码
5.5 关于模板是否可以定义虚函数
模板类是可以使用虚函数,但是类中的成员函数不可以定义为模板虚函数。 如果在类中将成员函数定义成模板虚函数的话,成员函数只有在定义了之后才算是实例化。这样在编译的时候,就不知道虚函数会实例化多少个,虚函数表的大小就没办法确定。但是在编译阶段,必须得确定类的虚函数表大小。所以不能将类中的成员函数定义为模板虚函数。
6. 运算操作符
6.1 x=x+1,x+=1,x++哪个效率高
C++
1.编译内存相关
1.1 C++程序编译过程
编译过程分为四个过程:编译(编译预处理、编译、优化),汇编,链接。
-
编译预处理:处理以 # 开头的指令;
-
编译、优化:将源码 .cpp 文件翻译成 .s 汇编代码;
-
汇编:将汇编代码 .s 翻译成机器指令 .o 文件;
-
链接:汇编程序生成的目标文件,即 .o 文件,并不会立即执行,因为可能会出现:.cpp 文件中的函数引用了另一个 .cpp 文件中定义的符号或者调用了某个库文件中的函数。那链接的目的就是将这些文件对应的目标文件连接成一个整体,从而生成可执行的程序 .exe 文件。
链接分为两种:
- 静态链接:代码从其所在的静态链接库中拷贝到最终的可执行程序中,在该程序被执行时,这些代码会被装入到该进程的虚拟地址空间中。
- 动态链接:代码被放到动态链接库或共享对象的某个目标文件中,链接程序只是在最终的可执行程序中记录了共享对象的名字等一些信息。在程序执行时,动态链接库的全部内容会被映射到运行时相应进行的虚拟地址的空间。
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5QrtRK4a-1649036213702)(C:\Users\ZHAOCHENHAO\Pictures\Camera Roll\image-20220308221305914.png)]
二者的优缺点:
-
静态链接:浪费空间,每个可执行程序都会有目标文件的一个副本,这样如果目标文件进行了更新操作,就需要重新进行编译链接生成可执行程序(更新困难);优点就是执行的时候运行速度快,因为可执行程序具备了程序运行的所有内容。
-
动态链接:节省内存、更新方便,但是动态链接是在程序运行时,每次执行都需要链接,相比静态链接会有一定的性能损失。
1.2 内存管理
C++ 内存分区:栈、堆、全局/静态存储区、常量存储区、代码区。
-
栈:存放函数的局部变量、函数参数、返回地址等,由编译器自动分配和释放。
-
堆:动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。
-
全局区/静态存储区(.bss 段和 .data 段):存放全局变量和静态变量,程序运行结束操作系统自动释放,在 C 语言中,未初始化的放在 .bss 段中,初始化的放在 .data 段中,C++ 中不再区分了。
-
常量存储区(.data 段):存放的是常量,不允许修改,程序运行结束自动释放。
-
代码区(.text 段):存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里。
说明:
- 从操作系统的本身来讲,以上存储区在内存中的分布是如下形式(从低地址到高地址):.text 段 --> .data 段 --> .bss 段 --> 堆 --> unused --> 栈 --> env
#include <iostream>
using namespace std;
/*
说明:C++ 中不再区分初始化和未初始化的全局变量、静态变量的存储区,如果非要区分下述程序标注在了括号中
*/
int g_var = 0; // g_var 在全局区(.data 段)
char *gp_var; // gp_var 在全局区(.bss 段)
int main()
{
int var; // var 在栈区
char *p_var; // p_var 在栈区
char arr[] = "abc"; // arr 为数组变量,存储在栈区;"abc"为字符串常量,存储在常量区
char *p_var1 = "123456"; // p_var1 在栈区;"123456"为字符串常量,存储在常量区
static int s_var = 0; // s_var 为静态变量,存在静态存储区(.data 段)
p_var = (char *)malloc(10); // 分配得来的 10 个字节的区域在堆区
free(p_var);
return 0;
}
1.3 栈和堆的区别
-
申请方式:栈是系统自动分配,堆是程序员主动申请。
-
申请后系统响应:分配栈空间,如果剩余空间大于申请空间则分配成功,否则分配失败栈溢出;申请堆空间,堆在内存中呈现的方式类似于链表(记录空闲地址空间的链表),在链表上寻找第一个大于申请空间的节点分配给程序,将该节点从链表中删除,大多数系统中该块空间的首地址存放的是本次分配空间的大小,便于释放,将该块空间上的剩余空间再次连接在空闲链表上。
-
栈在内存中是连续的一块空间(向低地址扩展)最大容量是系统预定好的,堆在内存中的空间(向高地址扩展)是不连续的。
-
申请效率:栈是有系统自动分配,申请效率高,但程序员无法控制;堆是由程序员主动申请,效率低,使用起来方便但是容易产生碎片。
-
存放的内容:栈中存放的是局部变量,函数的参数;堆中存放的内容由程序员控制。
1.4 变量的区别
全局变量、局部变量、静态全局变量、静态局部变量的区别 C++ 变量根据定义的位置的不同的生命周期,具有不同的作用域,作用域可分为 6 种:全局作用域,局部作用域,语句作用域,类作用域,命名空间作用域和文件作用域。
从作用域看:
-
全局变量:具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用 extern 关键字再次声明这个全局变量。
-
静态全局变量:具有文件作用域。它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被 static 关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。
-
局部变量:具有局部作用域。它是自动对象(auto),在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。
-
静态局部变量:具有局部作用域。它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。
从分配内存空间看:
- 静态存储区:全局变量,静态局部变量,静态全局变量。
- 栈:局部变量。
说明:
-
静态变量和栈变量(存储在栈中的变量)、堆变量(存储在堆中的变量)的区别:静态变量会被放在程序的静态数据存储区(.data 段)中(静态变量会自动初始化),这样可以在下一次调用的时候还可以保持原来的赋值。而栈变量或堆变量不能保证在下一次调用的时候依然保持原来的值。
-
静态变量和全局变量的区别:静态变量用 static 告知编译器,自己仅仅在变量的作用范围内可见。
1.5 全局变量定义在头文件中有什么问题?
如果在头文件中定义全局变量,当该头文件被多个文件 include
时,该头文件中的全局变量就会被定义多次,导致重复定义,因此不能再头文件中定义全局变量。
1.6 对象创建限制在堆或栈
说明:C++ 中的类的对象的建立分为两种:静态建立、动态建立。
-
静态建立:由编译器为对象在栈空间上分配内存,直接调用类的构造函数创建对象。例如:A a;
-
动态建立:使用 new 关键字在堆空间上创建对象,底层首先调用 operator new() 函数,在堆空间上寻找合适的内存并分配;然后,调用类的构造函数创建对象。例如:A *p = new A();
限制对象只能建立在堆上:
-
最直观的思想:避免直接调用类的构造函数,因为对象静态建立时,会调用类的构造函数创建对象。但是直接将类的构造函数设为私有并不可行,因为当构造函数设置为私有后,不能在类的外部调用构造函数来构造对象,只能用 new 来建立对象。但是由于 new 创建对象时,底层也会调用类的构造函数,将构造函数设置为私有后,那就无法在类的外部使用 new 创建对象了。因此,这种方法不可行。
-
解决方法 1:
将析构函数设置为私有。原因:静态对象建立在栈上,是由编译器分配和释放内存空间,编译器为对象分配内存空间时,会对类的非静态函数进行检查,即编译器会检查析构函数的访问性。当析构函数设为私有时,编译器创建的对象就无法通过访问析构函数来释放对象的内存空间,因此,编译器不会在栈上为对象分配内存。
class A
{
public:
A() {}
void destory()
{
delete this;
}
private:
~A()
{
}
};
该方法存在的问题:
- 用 new 创建的对象,通常会使用 delete 释放该对象的内存空间,但此时类的外部无法调用析构函数,因此类内必须定义一个 destory() 函数,用来释放 new 创建的对象。
- 无法解决继承问题,因为如果这个类作为基类,析构函数要设置成 virtual,然后在派生类中重写该函数,来实现多态。但此时,析构函数是私有的,派生类中无法访问。
- 解决方法2:
构造函数设置为 protected,并提供一个 public 的静态函数来完成构造,而不是在类的外部使用 new 构造;将析构函数设置为 protected。原因:类似于单例模式,也保证了在派生类中能够访问析构函数。通过调用 create() 函数在堆上创建对象。
class A
{
protected:
A() {}
~A() {}
public:
static A *create()
{
return new A();
}
void destory()
{
delete this;
}
};
限制对象只能建立在栈上:
- 解决方法:将 operator new() 设置为私有。原因:当对象建立在堆上时,是采用 new 的方式进行建立,其底层会调用 operator new() 函数,因此只要对该函数加以限制,就能够防止对象建立在堆上。
class A
{
private:
void *operator new(size_t t) {} // 注意函数的第一个参数和返回值都是固定的
void operator delete(void *ptr) {} // 重载了 new 就需要重载 delete
public:
A() {}
~A() {}
};
1.7 内存对齐
什么是内存对齐?内存对齐的原则?为什么要进行内存对齐,有什么优点?
内存对齐:编译器将程序中的每个“数据单元”安排在字的整数倍的地址指向的内存之中 内存对齐的原则:
-
结构体变量的首地址能够被其最宽基本类型成员大小与对齐基数中的较小者所整除;
-
结构体每个成员相对于结构体首地址的偏移量 (offset) 都是该成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在成员之间加上填充字节 (internal padding);
-
结构体的总大小为结构体最宽基本类型成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在最末一个成员之后加上填充字节 (trailing padding)。
/*
说明:程序是在 64 位编译器下测试的
*/
#include <iostream>
using namespace std;
struct A
{
short var; // 2 字节
int var1; // 8 字节 (内存对齐原则:填充 2 个字节) 2 (short) + 2 (填充) + 4 (int)= 8
long var2; // 12 字节 8 + 4 (long) = 12
char var3; // 16 字节 (内存对齐原则:填充 3 个字节)12 + 1 (char) + 3 (填充) = 16
string s; // 48 字节 16 + 32 (string) = 48
};
int main()
{
short var;
int var1;
long var2;
char var3;
string s;
A ex1;
cout << sizeof(var) << endl; // 2 short
cout << sizeof(var1) << endl; // 4 int
cout << sizeof(var2) << endl; // 4 long
cout << sizeof(var3) << endl; // 1 char
cout << sizeof(s) << endl; // 32 string
cout << sizeof(ex1) << endl; // 48 struct
return 0;
}
进行内存对齐的原因:(主要是硬件设备方面的问题)
-
某些硬件设备只能存取对齐数据,存取非对齐的数据可能会引发异常;
-
某些硬件设备不能保证在存取非对齐数据的时候的操作是原子操作;
-
相比于存取对齐的数据,存取非对齐的数据需要花费更多的时间;
-
某些处理器虽然支持非对齐数据的访问,但会引发对齐陷阱(alignment trap);
-
某些硬件设备只支持简单数据指令非对齐存取,不支持复杂数据指令的非对齐存取。
内存对齐的优点:
- 便于在不同的平台之间进行移植,因为有些硬件平台不能够支持任意地址的数据访问,只能在某些地址处取某些特定的数据,否则会抛出异常;
- 提高内存的访问效率,因为 CPU 在读取内存时,是一块一块的读取。
1.8 类的大小
说明:类的大小是指类的实例化对象的大小,用 sizeof
对类型名操作时,结果是该类型的对象的大小。 计算原则:
-
遵循结构体的对齐原则。
-
与普通成员变量有关,与成员函数和静态成员无关。即普通成员函数,静态成员函数,静态数据成员,静态常量数据成员均对类的大小无影响。因为静态数据成员被类的对象共享,并不属于哪个具体的对象。
-
虚函数对类的大小有影响,是因为虚函数表指针的影响。
-
虚继承对类的大小有影响,是因为虚基表指针带来的影响。
-
空类的大小是一个特殊情况,空类的大小为 1,当用 new 来创建一个空类的对象时,为了保证不同对象的地址不同,空类也占用存储空间。
/*
说明:程序是在 64 位编译器下测试的
*/
#include <iostream>
using namespace std;
class A
{
private:
static int s_var; // 不影响类的大小
const int c_var; // 4 字节
int var; // 8 字节 4 + 4 (int) = 8
char var1; // 12 字节 8 + 1 (char) + 3 (填充) = 12
public:
A(int temp) : c_var(temp) {} // 不影响类的大小
~A() {} // 不影响类的大小
};
class B
{
};
int main()
{
A ex1(4);
B ex2;
cout << sizeof(ex1) << endl; // 12 字节
cout << sizeof(ex2) << endl; // 1 字节
return 0;
}
/*
说明:程序是在 64 位编译器下测试的
*/
#include <iostream>
using namespace std;
class A
{
private:
static int s_var; // 不影响类的大小
const int c_var; // 4 字节
int var; // 8 字节 4 + 4 (int) = 8
char var1; // 12 字节 8 + 1 (char) + 3 (填充) = 12
public:
A(int temp) : c_var(temp) {} // 不影响类的大小
~A() {} // 不影响类的大小
virtual void f() { cout << "A::f" << endl; }
virtual void g() { cout << "A::g" << endl; }
virtual void h() { cout << "A::h" << endl; } // 24 字节 12 + 4 (填充) + 8 (指向虚函数的指针) = 24
};
int main()
{
A ex1(4);
A *p;
cout << sizeof(p) << endl; // 8 字节 注意:指针所占的空间和指针指向的数据类型无关
cout << sizeof(ex1) << endl; // 24 字节
return 0;
}
1.9 什么是内存泄漏
:由于疏忽或错误导致的程序未能释放已经不再使用的内存。
进一步解释:
-
并非指内存从物理上消失,而是指程序在运行过程中,由于疏忽或错误而失去了对该内存的控制,从而造成了内存的浪费。
-
常指堆内存泄漏,因为堆是动态分配的,而且是用户来控制的,如果使用不当,会产生内存泄漏。
-
使用 malloc、calloc、realloc、new 等分配内存时,使用完后要调用相应的 free 或 delete 释放内存,否则这块内存就会造成内存泄漏。
-
指针重新赋值
char *p = (char *)malloc(10);
char *p1 = (char *)malloc(10);
p = np;
开始时,指针 p
和 p1
分别指向一块内存空间,但指针 p
被重新赋值,导致 p
初始时指向的那块内存空间无法找到,从而发生了内存泄漏。
堆内存泄漏:new/mallc分配内存,未使用对应的delete/free回收 系统资源泄漏, Bitmap, handle,socket等资源未释放 没有将基类析构函数定义称为虚函数,(使用基类指针或者引用指向派生类对象时)派生类对象释放时将不能正确释放派生对象部分。
1.10 智能指针有哪几种?智能指针的实现原理?
智能指针是为了解决动态内存分配时带来的内存泄漏以及多次释放同一块内存空间而提出的。C++11 中封装在了 <memory>
头文件中。
C++11 中智能指针包括以下三种:
- 共享指针(shared_ptr):资源可以被多个指针共享,使用计数机制表明资源被几个指针共享。通过 use_count() 查看资源的所有者的个数,可以通过 unique_ptr、weak_ptr 来构造,调用 release() 释放资源的所有权,计数减一,当计数减为 0 时,会自动释放内存空间,从而避免了内存泄漏。
- 独占指针(unique_ptr):独享所有权的智能指针,资源只能被一个指针占有,该指针不能拷贝构造和赋值。但可以进行移动构造和移动赋值构造(调用 move() 函数),即一个 unique_ptr 对象赋值给另一个 unique_ptr 对象,可以通过该方法进行赋值。
- :指向
share_ptr
指向的对象,能够解决由shared_ptr带来的循环引用问题。
1.11 一个 unique_ptr 怎么赋值给另一个 unique_ptr 对象?
借助 std::move()
可以实现将一个 unique_ptr
对象赋值给另一个 unique_ptr
对象,其目的是实现所有权的转移。
// A 作为一个类
std::unique_ptr<A> ptr1(new A());
std::unique_ptr<A> ptr2 = std::move(ptr1);
1.12 使用智能指针会出现什么问题?怎么解决?
在如下例子中定义了两个类 Parent、Child,在两个类中分别定义另一个类的对象的共享指针,由于在程序结束后,两个指针相互指向对方的内存空间,导致内存无法释放。
#include <iostream>
#include <memory>
using namespace std;
class Child;
class Parent;
class Parent {
private:
shared_ptr<Child> ChildPtr;
public:
void setChild(shared_ptr<Child> child) {
this->ChildPtr = child;
}
void doSomething() {
if (this->ChildPtr.use_count()) {
}
}
~Parent() {
}
};
class Child {
private:
shared_ptr<Parent> ParentPtr;
public:
void setPartent(shared_ptr<Parent> parent) {
this->ParentPtr = parent;
}
void doSomething() {
if (this->ParentPtr.use_count()) {
}
}
~Child() {
}
};
int main() {
weak_ptr<Parent> wpp;
weak_ptr<Child> wpc;
{
shared_ptr<Parent> p(new Parent);
shared_ptr<Child> c(new Child);
p->setChild(c);
c->setPartent(p);
wpp = p;
wpc = c;
cout << p.use_count() << endl; // 2
cout << c.use_count() << endl; // 2
}
cout << wpp.use_count() << endl; // 1
cout << wpc.use_count() << endl; // 1
return 0;
}
weak_ptr
循环引用:该被调用的析构函数没有被调用,从而出现了内存泄漏。
-
weak_ptr
对被shared_ptr
管理的对象存在 ,在访问所引用的对象前必须先转化为shared_ptr
; -
weak_ptr 用来打断 shared_ptr 所管理对象的循环引用问题,若这种环被孤立(没有指向环中的外部共享指针),shared_ptr 引用计数无法抵达 0,内存被泄露;令环中的指针之一为弱指针可以避免该情况;
-
weak_ptr 用来表达临时所有权的概念,当某个对象只有存在时才需要被访问,而且随时可能被他人删除,可以用 weak_ptr 跟踪该对象;需要获得所有权时将其转化为 shared_ptr,此时如果原来的 shared_ptr 被销毁,则该对象的生命期被延长至这个临时的 shared_ptr 同样被销毁。
#include <iostream>
#include <memory>
using namespace std;
class Child;
class Parent;
class Parent {
private:
//shared_ptr<Child> ChildPtr;
weak_ptr<Child> ChildPtr;
public:
void setChild(shared_ptr<Child> child) {
this->ChildPtr = child;
}
void doSomething() {
//new shared_ptr
if (this->ChildPtr.lock()) {
}
}
~Parent() {
}
};
class Child {
private:
shared_ptr<Parent> ParentPtr;
public:
void setPartent(shared_ptr<Parent> parent) {
this->ParentPtr = parent;
}
void doSomething() {
if (this->ParentPtr.use_count()) {
}
}
~Child() {
}
};
int main() {
weak_ptr<Parent> wpp;
weak_ptr<Child> wpc;
{
shared_ptr<Parent> p(new Parent);
shared_ptr<Child> c(new Child);
p->setChild(c);
c->setPartent(p);
wpp = p;
wpc = c;
cout << p.use_count() << endl; // 2
cout << c.use_count() << endl; // 1
}
cout << wpp.use_count() << endl; // 0
cout << wpc.use_count() << endl; // 0
return 0;
}
2. 语言对比
2.1 C++ 11 新特性
auto
类型推导
auto
关键字:自动类型推导,编译器会在 通过初始值推导出变量的类型,通过 auto
定义的变量必须有初始值。
auto
关键字基本的使用语法如下:
auto var = val1 + val2; // 根据 val1 和 val2 相加的结果推断出 var 的类型,
lambda
表达式
lambda
表达式,又被称为 lambda
函数或者 lambda
匿名函数。
lambda
匿名函数的定义:
[capture list] (parameter list) -> return type
{
function body;
};
其中:
- capture list:捕获列表,指 lambda 所在函数中定义的局部变量的列表,通常为空。
- return type、parameter list、function body:分别表示返回值类型、参数列表、函数体,和普通函数一样。
举例:
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
int arr[4] = {4, 2, 3, 1};
//对 a 数组中的元素进行升序排序
sort(arr, arr+4, [=](int x, int y) -> bool{ return x < y; } );
for(int n : arr){
cout << n << " ";
}
return 0;
}
- 右值引用***
右值引用的出现是为了解决两个问题的,第一个问题是临时对象非必要的昂贵的拷贝操作,第二个问题是在模板函数中如何按照参数的实际类型进行转发。通过右值引用,很好的解决两个问题。
引用,就是为了避免复制而存在,而左值引用和右值引用是为了不同的对象存在:
- 左值引用的对象是变量
- 右值引用的对象是常量
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int var = 42;
int &l_var = var;
int &&r_var = var; // error: cannot bind rvalue reference of type 'int&&' to lvalue of type 'int' 错误:不能将右值引用绑定到左值上
int &&r_var2 = var + 40; // 正确:将 r_var2 绑定到求和结果上
return 0;
}
- 智能指针 相关知识已在第一章中进行了详细的说明,这里不再重复。
2.2 C和C++的区别
首先说一下面向对象和面向过程:
面向过程的思路:分析解决问题所需的步骤,用函数把这些步骤依次实现。 面向对象的思路:把构成问题的事务分解为各个对象,建立对象的目的,不是完成一个步骤,而是描述某个事务在解决整个问题步骤中的行为。
区别和联系:
-
语言自身:C 语言是面向过程的编程,它最重要的特点是函数,通过 main 函数来调用各个子函数。程序运行的顺序都是程序员事先决定好的。C++ 是面向对象的编程,类是它的主要特点,在程序执行过程中,先由主 main 函数进入,定义一些类,根据需要执行类的成员函数,过程的概念被淡化了(实际上过程还是有的,就是主函数的那些语句。),以类驱动程序运行,类就是对象,所以我们称之为面向对象程序设计。面向对象在分析和解决问题的时候,将涉及到的数据和数据的操作封装在类中,通过类可以创建对象,以事件或消息来驱动对象执行处理。
-
应用领域:C 语言主要用于嵌入式领域,驱动开发等与硬件直接打交道的领域,C++ 可以用于应用层开发,用户界面开发等与操作系统打交道的领域。
-
C++ 既继承了 C 强大的底层操作特性,又被赋予了面向对象机制。它特性繁多,面向对象语言的多继承,对值传递与引用传递的区分以及 const 关键字,等等。
-
C++ 对 C 的“增强”,表现在以下几个方面:类型检查更为严格。增加了面向对象的机制、泛型编程的机制(Template)、异常处理、运算符重载、标准模板库(STL)、命名空间(避免全局命名冲突)。
3. 面向对象
3.1 什么是面向对象
面向对象:对象是指具体的某一个事物,这些事物的抽象就是类,类中包含数据(成员变量)和动作(成员方法)。
面向对象的三大特性:
-
封装:将具体的实现过程和数据封装成一个函数,只能通过接口进行访问,降低耦合性。
-
继承:子类继承父类的特征和行为,子类有父类的非 private 方法或成员变量,子类可以对父类的方法进行重写,增强了类之间的耦合性,但是当父类中的成员变量、成员函数或者类本身被 final 关键字修饰时,修饰的类不能继承,修饰的成员不能重写或修改。
-
多态:多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式。
3.2 重载、重写、隐藏的区别
- 重载:是指同一可访问区内被声明几个具有不同参数列(参数的类型、个数、顺序)的同名函数,根据参数列表确定调用哪个函数,
class A
{
public:
void fun(int tmp);
void fun(float tmp); // 重载 参数类型不同(相对于上一个函数)
void fun(int tmp, float tmp1); // 重载 参数个数不同(相对于上一个函数)
void fun(float tmp, int tmp1); // 重载 参数顺序不同(相对于上一个函数)
int fun(int tmp); // error: 'int A::fun(int)' cannot be overloaded 错误:注意重载不关心函数返回类型
};
- 隐藏:是指派生类的函数屏蔽了与其同名的基类函数,主要只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。
#include <iostream>
using namespace std;
class Base
{
public:
void fun(int tmp, float tmp1) { cout << "Base::fun(int tmp, float tmp1)" << endl; }
};
class Derive : public Base
{
public:
void fun(int tmp) { cout << "Derive::fun(int tmp)" << endl; } // 隐藏基类中的同名函数
};
int main()
{
Derive ex;
ex.fun(1); // Derive::fun(int tmp)
ex.fun(1, 0.01); // error: candidate expects 1 argument, 2 provided
return 0;
}
说明:上述代码中 ex.fun(1, 0.01); 出现错误,说明派生类中将基类的同名函数隐藏了。若是想调用基类中的同名函数,可以加上类型名指明 ex.Base::fun(1, 0.01);,这样就可以调用基类中的同名函数。
- 重写(覆盖):是指派生类中存在重新定义的函数。函数名、参数列表、返回值类型都必须同基类中被重写的函数一致,只有函数体不同。派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有 virtual 修饰。
:
-
范围区别:对于类中函数的重载或者重写而言,重载发生在同一个类的内部,重写发生在不同的类之间(子类和父类之间)。
-
参数区别:重载的函数需要与原函数有相同的函数名、不同的参数列表,不关注函数的返回值类型;重写的函数的函数名、参数列表和返回值类型都需要和原函数相同,父类中被重写的函数需要有 virtual 修饰。
-
virtual 关键字:重写的函数基类中必须有 virtual关键字的修饰,重载的函数可以有 virtual 关键字的修饰也可以没有。
:
-
范围区别:隐藏与重载范围不同,隐藏发生在不同类中。
-
参数区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定相同;当参数不同时,无论基类中的函数是否被 virtual 修饰,基类函数都是被隐藏,而不是重写。
3.3 什么是多态?多态如何实现?
:多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式。在基类的函数前加上 virtual 关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
:多态是通过虚函数实现的,虚函数的地址保存在虚函数表中,虚函数表的地址保存在含有虚函数的类的实例对象的内存空间中。
:
-
在类中用 virtual 关键字声明的函数叫做虚函数;
-
存在虚函数的类都有一个虚函数表,当创建一个该类的对象时,该对象有一个指向虚函数表的虚表指针(虚函数表和类对应的,虚表指针是和对象对应);
-
当基类指针指向派生类对象,基类指针调用虚函数时,基类指针指向派生类的虚表指针,由于该虚表指针指向派生类虚函数表,通过遍历虚表,寻找相应的虚函数。
基类的虚函数表如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hb5aTr11-1649036213703)(C:\Users\ZHAOCHENHAO\Pictures\Camera Roll\1612675767-guREBN-image.png)]
派生类的对象虚函数表如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ckZj0DAW-1649036213704)(C:\Users\ZHAOCHENHAO\Pictures\Camera Roll\1618818155-PZxTzJ-image.png)]
4. 关键字库函数
4.1 sizeof 和 strlen 的区别
strlen
是头文件 中的函数,sizeof
是 C++ 中的运算符。strlen
测量的是字符串的实际长度(其源代码如下),以\0
结束。而sizeof
测量的是字符数组的分配大小。
strlen
源代码:
size_t strlen(const char *str) {
size_t length = 0;
while (*str++)
++length;
return length;
}
举例:
#include <iostream>
#include <cstring>
using namespace std;
int main()
{
char arr[10] = "hello";
cout << strlen(arr) << endl; // 5
cout << sizeof(arr) << endl; // 10
return 0;
}
- 若字符数组 arr 作为函数的形参,sizeof(arr) 中 arr 被当作字符指针来处理,strlen(arr) 中 arr 依然是字符数组,从下述程序的运行结果中就可以看出。
#include <iostream>
#include <cstring>
using namespace std;
void size_of(char arr[])
{
cout << sizeof(arr) << endl; // warning: 'sizeof' on array function parameter 'arr' will return size of 'char*' .
cout << strlen(arr) << endl;
}
int main()
{
char arr[20] = "hello";
size_of(arr);
return 0;
}
/*
输出结果:
8
5
*/
-
strlen
本身是库函数,因此在程序运行过程中,计算长度;而sizeof
在编译时,计算长度; -
sizeof
的参数可以是类型,也可以是变量;strlen
的参数必须是char*
类型的变量。
4.2 lambda 表达式(匿名函数)的具体应用和使用场景
lambda
表达式的定义形式如下:
[capture list] (parameter list) -> reurn type
{
function body
}
其中:
-
capture list:捕获列表,指 lambda 表达式所在函数中定义的局部变量的列表,通常为空,但如果函数体中用到了 lambda 表达式所在函数的局部变量,必须捕获该变量,即将此变量写在捕获列表中。捕获方式分为:引用捕获方式 [&]、值捕获方式 [=]。
-
return type、parameter list、function body:分别表示返回值类型、参数列表、函数体,和普通函数一样。
举例: lambda
表达式常搭配排序算法使用。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
vector<int> arr = {3, 4, 76, 12, 54, 90, 34};
sort(arr.begin(), arr.end(), [](int a, int b) { return a > b; }); // 降序排序
for (auto a : arr)
{
cout << a << " ";
}
return 0;
}
/*
运行结果:90 76 54 34 12 4 3
*/
4.3 explicit 的作用(如何避免编译器进行隐式类型转换)
作用:用来声明类构造函数是显示调用的,而非隐式调用,可以阻止调用构造函数时进行隐式转换。只可用于修饰