c++ 面试指南_Σίσυφος1900的博客-CSDN博客接着上一篇,主要是给自己看。后期维护,一辈子只要能学懂一门语言就可以了。
Leetcode——C++突击面试_Stephen-CSDN博客
必备C++面试题大全含PDF版! - 哔哩哔哩
12、typedef和define,以及 define 和 const 的区别
define 和 const 的区别
区别:
- 编译阶段:define 是在编译预处理阶段进行替换,const 是在编译阶段确定其值。
- 安全性:define 定义的宏常量没有数据类型,只是进行简单的替换,不会进行类型安全的检查;const 定义的常量是有类型的,是要进行判断的,可以避免一些低级的错误。
- 内存占用:define 定义的宏常量,在程序中使用多少次就会进行多少次替换,内存中有多个备份,占用的是代码段的空间;const 定义的常量占用静态存储区的空间,程序运行过程中只有一份。
- 调试:define 定义的宏常量不能调试,因为在预编译阶段就已经进行替换了;cons定义的常量可以进行调试。
const 的优点:
- 有数据类型,在定义式可进行安全性检查。
- 可调式。
- 占用较少的空间。
-
define 和 typedef 的区别
- 原理:#define 作为预处理指令,在编译预处理时进行替换操作,不作正确性检查,只有在编译已被展开的源程序时才会发现可能的错误并报错。typedef 是关键字,在编译时处理(和const 是一样的),有类型检查功能,用来给一个已经存在的类型一个别名,但不能在一个函数定义里面使用 typedef 。
- 功能:typedef 用来定义类型的别名,方便使用。#define 不仅可以为类型取别名,还可以定义常量、变量、编译开关等。
- 作用域:#define 没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而 typedef 有自己的作用域。
- 指针的操作:typedef 和 #define 在处理指针时不完全一样
13、简述strcpy、sprintf与memcpy的区别
strcpy
char*strcpy(char *dest, const char *src); 遇到\0结束(\0也被复制了),只能拷贝字符串。其对字符串进行操作,完成从源字符串到目的字符串的拷贝,当源字符串的大小大于目的字符串的最大存储空间后,执行该操作会出现段错误
har *strcpy(char *strDest, const char *strSrc);// 实现strSrc到strDest的复制
{
if ((strSrc == NULL) || (strDest == NULL)) //判断参数有效性
{
return NULL;
}
char *dest = strDest; //保存目标字符串的首地址
while ((*strDest++ = *strSrc++)!='\0'); //把src字符串的内容复制到dest下
return dest;
}
sprintf sprintf 可以用%s来实现格式化写入 主要实现其他数据类型格式到字符串的转换;
memcpy 主要是内存块间的拷贝 。根据size大小来复制,可以复制各种数据类型(结构体、数组)。两个对象就是两个任意可操作的内存地址,并不限于何种数据类型
void *memcpy(void *memTo, const void *memFrom, size_t size)
{
if((memTo == NULL) || (memFrom == NULL)) //memTo和memFrom必须有效
return NULL;
char *tempFrom = (char *)memFrom; //保存memFrom首地址
char *tempTo = (char *)memTo; //保存memTo首地址
while(size -- > 0) //循环size次,复制memFrom的值到memTo中
*tempTo++ = *tempFrom++ ;
return memTo;
}
执行效率不同
memcpy 最高(内存快之间的拷贝),strcpy 次之,sprintf 最低
引申:如何判断结构体是否相等?能否用 memcmp 函数判断结构体相等?
需要重载操作符 ==》 判断两个结构体是否相等
#if 1 // 用重载操作的方法对比两个结构体是否相等
struct S
{
// 结构体类型
char c;
int val;
double d2;
S(char c_tmp, int tmp,double d) : c(c_tmp), val(tmp),d2(d) {}
friend bool operator==(const S& tmp1, const S& tmp2); // 友元运算符重载函数
};
bool operator==(const S& tmp1, const S& tmp2)
{
return (tmp1.c == tmp2.c && tmp1.val == tmp2.val&& tmp1.d2 == tmp2.d2);
}
int main()
{
S s1('a', 23,234455.0);
S s2('e', 3, 455.0);
if (s1 == s2)
cout << "s1 == s2" << endl;
else
cout << "s1 != s2" << endl;
return 0;
return 0;
}
#endif
不能用函数 memcmp 来判断两个结构体是否相等,因为 memcmp 函数是逐个字节进行比较的,而结构体存在内存空间中保存时存在字节对齐,字节对齐时补的字节内容是随机的,会产生垃圾值,所以无法比较。
14、C++中有了malloc / free , 为什么还需要 new / delete?
new的作用:
new 是一个关键字,用来动态的分配内存空间,
int * p=new int[19];
new 和malloc 如何判断申请到了空间?
- malloc :成功-》返回改内存的指针,失败-》返回NULL指针
- new :成功-》返回该对象类型的指针,失败,抛出 bac_alloc 异常。
new和malloc 以及delete 和free 的区别
- malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符(是运算符不是库函数)。它们都可用于申请动态内存和释放内存。
- 由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
- new 在申请内存的时候就可以初始化 int *p = new int(1);, 而malloc是不允许的。另外,由于malloc是库函数,需要相应的库支持,因此某些简易的平台可能不支持,但是new就没有这个问题了,因为new是C++语言所自带的运算符
- new 申请空间时,无需指定分配空间的大小,编译器会根据类型自行计算;malloc 在申请空间时,需要确定所申请空间的大小
- new 分配失败时,会抛出 bad_alloc 异常,malloc 分配失败时返回空指针
- new 申请空间时,返回的类型是对象的指针类型,无需强制类型转换,是类型安全的操作符;malloc 申请空间时,返回的是 void* 类型,需要进行强制类型的转换,转换为对象类型的指针
- new 操作符从自由存储区上为对象动态分配内存,而 malloc 函数从堆上动态分配内存。
- 对于自定义的类型,new 首先调用 operator new() 函数申请空间(底层通过 malloc 实现),然后调用构造函数进行初始化,最后返回自定义类型的指针;delete 首先调用析构函数,然后调用 operator delete() 释放空间(底层通过 free 实现)。malloc、free 无法进行自定义类型的对象的构造和析构。
malloc 的原理?malloc 的底层实现?
malloc 的原理:
- 当开辟的空间小于 128K 时,调用 brk() 函数,通过移动 _enddata 来实现;
- 当开辟空间大于 128K 时,调用 mmap() 函数,通过在虚拟地址空间中开辟一块内存空间来实现。
malloc 的底层实现:
-
brk() 函数实现原理:向高地址的方向移动指向数据段的高地址的指针 _enddata。
-
mmap 内存映射原理:
1.进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域;
2.调用内核空间的系统调用函数 mmap(),实现文件物理地址和进程虚拟地址的一一映射关系;
3.进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝。
缺点:容易造成内存泄漏和过多的内存碎片,影响系统正常运行,还得注意判断内存是否分配成功,而且内存释放后(使用free函数之后指针变量p本身保存的地址并没有改变),需要将p的赋值为NULL拴住野指
为什么不全部使用mmap来分配内存?
为向操作系统申请内存的时候,是要通过系统调用的,执行系统调用要进入内核态,然后再回到用户态,状态的切换会耗费不少时间,所以申请内存的操作应该避免频繁的系统调用,如果都使用mmap来分配内存,等于每次都要执行系统调用。另外,因为mmap分配的内存每次释放的时候都会归还给操作系统,于是每次mmap分配的虚拟地址都是缺页状态,然后在第一次访问该虚拟地址的时候就会触发缺页中断。
为什么不全部都用brk
如果全部使用brk申请内存那么随着程序频繁的调用malloc和free,尤其是小块内存,堆内将产生越来越多的不可用的内存碎片。
15、C 的结构体和 C++ 的有什么区别
- C中的结构体就不存在面向对象的任何特点:不能继承;不能封装;不能多态;C++ 中 struct 可以和类一样,有访问权限,并可以定义成员函数。
- C不存在访问控制,只有作用域,不能定义成员函数; C++ 中 struct 是抽象数据类型,支持成员函数的定义。
- C语言中的结构体不能为空,否则会报错。定义一个结构体的变量时,结构体名前的struct关键字不能省略,否则会报错;而 C++ 中,不用加该关键字,例如:A var;
为什么有了 class 还保留 struct?
- C++ 是在 C 语言的基础上发展起来的,为了与 C 语言兼容,C++ 中保留了 struct。
c++ 中 struct和class的区别
- struct 和 class 都可以自定义数据类型,也支持继承操作。
- struct 中默认的访问级别是 public,默认的继承级别也是 public;class 中默认的访问级别是 private,默认的继承级别也是 private。
- 当 class 继承 struct 或者 struct 继承 class 时,默认的继承级别取决于 class 或 struct 本身,class(private 继承),struct(public 继承),即取决于派生类的默认继承级别。
- class 可以用于定义模板参数,struct 不能用于定义模板参数
#if 1
// C++ struct 和class 的区别
class A
{
public:
void Func1()
{
cout << "class A -> Func1() " << endl;
}
};
struct B:public A // 结构体可以继承类
{
void StructFunc()
{
cout << "struct B -> StructFunc() " << endl;
}
};
class C : public B // 类可以继承 结构体 注意我这些的是public :B ,默认是private:B
{
public:
void CFunc()
{
cout << "class C : public B -> CFunc() " << endl;
}
};
int main()
{
A a; // 类
a.Func1();// class A
B b; // 结构体
b.StructFunc();
b.Func1();// 从Class A中继承来的func()
C c;
c.CFunc();//
c.StructFunc();
c.Func1();
return 0;
}
#endif // 0
16、 内存对齐
进行内存对齐的原因:(主要是硬件设备方面的问题)
什么是内存对齐?内存对齐的原则?为什么要进行内存对齐,有什么优点?
内存对齐:编译器将程序中的每个“数据单元”安排在字的整数倍的地址指向的内存之中
内存对齐的原则:
- 结构体变量的首地址能够被其最宽基本类型成员大小与对齐基数中的较小者所整除;
- 结构体每个成员相对于结构体首地址的偏移量 (offset)
都是该成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在成员之间加上填充字节 (internal padding); - 结构体的总大小为结构体最宽基本类型成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。
- 某些硬件设备只能存取对齐数据,存取非对齐的数据可能会引发异常;
- 某些硬件设备不能保证在存取非对齐数据的时候的操作是原子操作;
- 相比于存取对齐的数据,存取非对齐的数据需要花费更多的时间;
- 某些处理器虽然支持非对齐数据的访问,但会引发对齐陷阱(alignmenttrap);
- 某些硬件设备只支持简单数据指令非对齐存取,不支持复杂数据指令的非对齐存取。
- 便于在不同的平台之间进行移植,因为有些硬件平台不能够支持任意地址的数据访问,只能在某些地址处取某些特定的数据,否则会抛出异常;
- 提高内存的访问效率,因为 CPU 在读取内存时,是一块一块的读取。
内存对齐的优点:
- 便于在不同的平台之间进行移植,因为有些硬件平台不能够支持任意地址的数据访问,只能在某些地址处取某些特定的数据,否则会抛出异常;
- 提高内存的访问效率,因为 CPU 在读取内存时,是一块一块的读取。
-
struct 和 union 的区别
说明:union 是联合体,struct 是结构体。
区别:
- 联合体和结构体都是由若干个数据类型不同的数据成员组成。使用时,联合体只有一个有效的成员;而结构体所有的成员都有效。
- 对联合体的不同成员赋值,将会对覆盖其他成员的值,而对于结构体的对不同成员赋值时,相互不影响。
- 联合体的大小为其内部所有变量的最大值,按照最大类型的倍数进行分配大小;结构体分配内存的大小遵循内存对齐原则。
16、C和C++中的强制类型转换?
C:C中是直接在变量或者表达式前面加上(小括号括起来的)目标类型来进行转换,一招走天下,操作简单,但是由于太过直接,缺少检查,因此容易发生编译检查不到错误,而人工检查又及其难以发现的情况。
C++:
1)static_cast
-
用于基本类型间的转换 ,可以将空指针转化成目标类型的空指针,可以将任何类型的表达式转化成 void 类型
-
不能用于基本类型指针间的转换
-
用于有继承关系类对象间的转换和类指针间的转换
2)dynamic_cast
-
用于有继承关系的类指针间的转换
-
用于有交叉关系的类指针间的转换
-
具有类型检查的功能
-
需要虚函数的支持
3)reinterpret_cast
-
用于指针和引用类型 间的类型转换
-
用于整数和指针间的类型转换
4)const_cast
-
用于去掉变量的const属性
-
转换的目标类型必须是指针或者引用
17、在C++程序中调用被C编译器编译后的函数,为什么要加extern“C”?
C++语言支持函数重载,C语言不支持函数重载,函数被C++编译器编译后在库中的名字与C语言的不同,假设某个函数原型为:
void fun(int x,int y);
该函数被C编译器编译后在库中的名字为 _fun, 而C++编译器则会产生像: _fun_int_int 之类的名字。为了解决此类名字匹配的问题,C++提供了C链接交换指定符号 extern “C”
18、什么是内存泄漏?内存泄露检测原理是什么?怎么防止内存泄漏?
用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元即为内存泄露。一般的都是在堆上面的内存泄露。举例说明:
int *pt=(int *)malloc(sizeof(int)*30); ;
int *p = (int *)malloc(sizeof(int)*20); // p-->ox887778
int *p1 =(int *)malloc(sizeof(int)*10); // p1-->ox885665
p = pt; // 这个时候p指向了前面pt的那个30个int大小的空之间,20 的那个空间没有释放,也没有别的指针指向,就成被泄露的内存
内存泄漏检测工具的实现原理:
valgrind ,或者运行的时候发现内存随时间不断的增长,可以是内存泄露导致。
解决:
1). 使用的时候要记得指针的长度.
2). malloc的时候得确定在那里free.(堆上面)
3). 对指针赋值的时候应该注意被赋值指针需要不需要释放.
4). 动态分配内存的指针最好不要再次赋值.
5). 在C++中应该优先考虑使用智能指针.
怎么防止内存泄漏?内存泄漏检测工具的原理?
防止内存泄漏的方法:
内部封装:将内存的分配和释放封装到类中,在构造的时候申请内存,析构的时候释放内存。(说明:但这样做并不是最佳的做法,在类的对象复制时,程序会出现同一块内存空间释放两次的情况)
智能指针:智能指针是 C++ 中已经对内存泄漏封装好了一个工具,可以直接拿来使用,将在下一个问题中对智能指针进行详细的解释。
VS下内存泄漏的检测方法(CRT):
#if 1
#define CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
//在入口函数中包含 _CrtDumpMemoryLeaks();
//即可检测到内存泄露
//以如下测试函数为例:
int main()
{
char* pChars = new char[10];
_CrtDumpMemoryLeaks();
return 0;
}
#endif // 0
19、什么是野指针 什么是悬空指针?如何避免野指针
//其指向没有确定指向什么地方,定义完后没有使用,没有初始化 int *p; // p 是“野指针”。
// 悬空指针:若指针指向一块内存空间,当这块内存空间被释放后,该指针依然指向这块内存空间, //此时,称该指针为“悬空指针”。 int*p = (int *)malloc(sizeof(int)*10); free(p); //一般情况下我们会在后面加上一句 p=NULL;
如何避免野指针?
- 指针变量声明时没有被初始化。解决办法:指针声明时初始化,可以是具体的地址值,也可让它指向NULL。
- 指针p被free或者delete之后,没有置为NULL。解决办法:指针指向的内存空间被释放后指针应该指向NULL。
- 指针操作超越了变量的作用范围。解决办法:在变量的作用域结束前释放掉变量的地址空间并且让指针指向NULL。
20、什么是智能指针,有哪几种,使用智能指针会出现什么问题?其实现原理是什么?
智能指针的提出就是为了解决动态内存分配导致的内存泄露问题,以及多次释放的问题。
种类:有三种 共享指针(shared_ptr)、独占指针(unique_ptr)、弱指针(weak_ptr);
-
共享指针(shared_ptr):资源可以被多个指针共享,使用计数机制表明资源被几个指针共享。通过 use_count() 查看资源的所有者的个数,可以通过 unique_ptr、weak_ptr 来构造,调用 release() 释放资源的所有权,计数减一,当计数减为 0 时,会自动释放内存空间,从而避免了内存泄漏。
-
独占指针(unique_ptr):独享所有权的智能指针,资源只能被一个指针占有,该指针不能拷贝构造和赋值。但可以进行移动构造和移动赋值构造(调用move() 函数),即一个 unique_ptr 对象赋值给另一个 unique_ptr 对象,可以通过该方法进行赋值。
// 将一个unique_ptr 指针赋值给另一个unique_ptr对象 // A 作为一个类 std::unique_ptr<A> ptr1(new A()); std::unique_ptr<A> ptr2 = std::move(ptr1); move 的原理上一篇已经讲了
-
弱指针(weak_ptr):指向 shared_ptr 指向的对象,能够解决由shared_ptr带来的循环引用问题。
21、C++ 11 新特性
1. auto 类型推导-(个人的理解是和java中的反射是一样的)
auto 关键字:自动类型推导,编译器会在 编译期间 通过初始值推导出变量的类型,通过 auto 定义的变量必须有初始值。
2. decltype 类型推导
decltype 关键字:decltype 是“declare type”的缩写,译为“声明类型”。和 auto 的功能一样,都用来在编译时期进行自动类型推导。如果希望从表达式中推断出要定义的变量的类型,但是不想用该表达式的值初始化变量,这时就不能再用 auto。decltype 作用是选择并返回操作数的数据类型。
区别:
int v1 = 20;
int v2 = 30;
auto v3 = v1 + v2;
cout << "atuo :" << v3 << endl; //50
decltype(v1 + v1) v4 = 0;
cout << "decltype :" << sizeof(v4) << endl; // 4
double vv = 8;
double vv2 = 8;
decltype(vv + vv2) v5 = 0;
cout << "decltype :" << sizeof(v5) << endl; //8
return 0;
- auto 根据 = 右边的初始值 val1 + val2 推导出变量的类型,并将该初始值赋值给变量 var;decltype 根据 val1 + val2 表达式推导出变量的类型,变量的初始值和与表达式的值无关。
- auto 要求变量必须初始化,因为它是根据初始化的值推导出变量的类型,而 decltype 不要求,定义变量的时候可初始化也可以不初始化。
3. lambda 表达式
lambda 表达式,又被称为 lambda 函数或者 lambda 匿名函数。
lambda匿名函数的定义:
[capture list] (parameter list) -> return type
{
function body;
};
其中:
- capture list:捕获列表,指 lambda 所在函数中定义的局部变量的列表,通常为空。
- return type、parameter list、function body:分别表示返回值类型、参数列表、函数体,和普通函数一样。
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;
}
4. 范围 for 语句
参数的含义:
- expression:必须是一个序列,例如用花括号括起来的初始值列表、数组、vector ,string等,这些类型的共同特点是拥有能返回迭代器的 beign、end 成员。
- declaration:此处定义一个变量,序列中的每一个元素都能转化成该变量的类型,常用 auto 类型说明符。
vector<Person*> v; Person p1("aaa", 10); Person p2("bbb", 10); Person p3("ccc", 20); Person p4("ddd", 30); Person p5("eee", 40); Person p6("fff", 50); v.push_back(&p1); v.push_back(&p2); v.push_back(&p3); v.push_back(&p5); v.push_back(&p4); v.push_back(&p6); for (auto e:v) { cout << " 姓名: " << e->m_Name << " 年龄是: " << e->m_age << endl; }
5. 右值引用
右值引用是为一个临时变量取别名,它只能绑定到一个临时变量或表达式(将亡值)上。实际开发中我们可能需要对右值进行修改(实现移动语义时就需要)而右值引用可以对右值进行修改。
为什么:
1.为了支持移动语义,右值引用可以绑定到临时对象、表达式等右值上,这些右值在生命周期结束后就会被销毁,因此可以在右值引用中窃取其资源,从而避免昂贵的复制操作,实现高效的移动语义。
2.完美转发:右值引用可以绑定到任何类型的右值上,可以将其作为参数传递给函数,并在函数内部将其“转发”到其他函数中,从而实现完美转发。
3.拓展可变参数模板,实现更加灵活的模板编程。
右值引用:绑定到右值的引用,用 && 来获得右值引用,右值引用只能绑定到要销毁的对象。为了和右值引用区分开,常规的引用称为左值引用。
int main()
{
int v = 2;
int& v2 = v;
cout << "v2: "<<v2 <<" v: "<< v << endl;
//int&& v2 = v; // 错误:不能将右值引用绑定到左值上 & v2
int&& v3 = v2 + 3445; // 正确:将 v2 绑定到求和结果上
cout << "v3: " << v3 << " v: " << v << " v2 :"<<v2 << endl;
return 0;
}
6. 标准库 move() 函数
7. 智能指针
8. delete 函数和 default 函数
- delete 函数:= delete 表示该函数不能被调用。
- default 函数:= default 表示编译器生成默认的函数,例如:生成默认的构造函数。
# if 1 //delete 函数和 default 函数
class A
{
public:
A() = default; // 表示使用默认的构造函数
~A() = default; // 表示使用默认的析构函数
A(const A&) = delete; // 表示类的对象禁止拷贝构造
A& operator=(const A&) = delete; // 表示类的对象禁止拷贝赋值
};
int main()
{
A ex1;
A ex2 = ex1; // error: use of deleted function 'A::A(const A&)'
A ex3;
ex3 = ex1; // error: use of deleted function 'A& A::operator=(const A&)'
return 0;
}
#endif
22、C 和 C++ 的区别
C :面向过程的思路
C++:人就是一个对象,性格,学识等是人的属性,体现人的行为
- C++ 既继承了 C 强大的底层操作特性,又被赋予了面向对象机制。它特性繁多,面向对象语言的多继承,对值传递与引用传递的区分以及 const 关键字,等等。
- C++ 对 C 的“增强”,表现在以下几个方面:类型检查更为严格。增加了面向对象的机制、泛型编程的机制(Template)、异常处理、运算符重载、标准模板库(STL)、命名空间(避免全局命名冲突)
23、 什么是面向对象?面向对象的三大特性
面向对象:对象是指具体的某一个事物,这些事物的抽象就是类,类中包含数据(成员变量)和动作(成员方法)。
面向对象的三大特性:
- 封装:将具体实现过程和数据封装成一个函数,只能通过接口进行访问,降低耦合性,使类成为一个具有内部数据的自我隐藏能力、功能独立的软件模块。 意义:保护或防止代码在无意之中被破坏,保护类中的成员,不让类中以外的程序直接访问或者修改,只能通过提供的公共接口访问。
- 继承:子类继承父类的特征和行为,复用了基类的全体数据和成员函数,具有从基类复制而来的数据成员和成员函数(基类私有成员可被继承,但是无法被访问),其中构造函数、析构函数、友元函数、静态数据成员、静态成员函数都不能被继承。基类中成员的访问方式只能决定派生类能否访问它们。增强了代码耦合性,当父类中的成员变量或者类本身被final关键字修饰时,修饰的类不能被继承,修饰的成员变量不能重写或修改。意义:基类的程序代码可以被派生类服用,提高了软件复用的效率,缩短了软件开发的周期
- 多态:多态是指通过基类的指针或者引用指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式,在运行时动态调用实际绑定对象函数的行为。与之相对应的编译时绑定函数称为静态绑定。多态是设计模式的基础,多态是框架的基础。
史上最全C/C++面试八股文,一文带你彻底搞懂C/C++面试!_c++_Accepted1024-GitCode 开源社区
final标识符的作用是什么?
放在类的后面表示该类无法被继承,也就是阻止了从类的继承,放在虚函数后面该虚函数无法被重写,表示阻止虚函数的重载
24、重载、重写、隐藏的区别
- 重载:函数名相同,但是参数的类型个数顺序不同,不关心返回值的问题
class Overloaded { public: void fun(int a); void fun(float a); void fun(int a, float b); void fun(float a, int b); int fun(int a, double c); // int fun(int a) 报错 int fun(int a);// error: 'int A::fun(int)' cannot be overloaded 错误:注意重载不关心函数返回类型 };
- 重写:和继承有关,子类中重写父类的函数或者方法,函数名、参数列表、返回值类型都必须同基类中被重写的函数一致,只有函数体不同。
-
// 动态多肽 父类的指针或者引用指向子类 class Animals { public: int m_A; virtual void speak() { cout << "动物在说话" << endl; } }; class Dog :public Animals { public: void speak() { cout << "小狗在说话" << endl; } }; // 动态多肽: 父类的指针或者引用指向子类Animals& animals void doSpeak2(Animals& animals) //如果是想要猫说话就不能让函数的地址早绑定 { animals.speak(); } void test2() // 动态多肽 { Dog dog; doSpeak2(dog); } int main() { test2(); }
- 隐藏:是指派生类的函数屏蔽了与其同名的基类函数,主要只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。
重写和重载的区别:
- 范围区别:重载发生在同一个类的内部,重写发生在不同的类之间(子类和父类之间)。
- 参数区别:重载的函数需要与原函数有相同的函数名、不同的参数列表,不关注函数的返回值类型;重写的函数的函数名、参数列表和返回值类型都需要和原函数相同,父类中被重写的函数需要有 virtual 修饰。
- virtual 关键字:重写的函数基类中必须有 virtual关键字的修饰,重载的函数可以有 virtual 关键字的修饰也可以没有。
隐藏和重写,重载的区别:
- 范围区别:隐藏与重载范围不同,隐藏发生在不同类中。
- 参数区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定相同;当参数不同时,无论基类中的函数是否被 virtual
修饰,基类函数都是被隐藏,而不是重写。
25、什么是多态?多态如何实现?
多肽:父类的指针指或者引用向子类的对象,在基类的函数前加上 virtual 关键字,在派生类中重写该函数,如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数
多肽的实现
静态多肽 和 动态多肽
动态多态
通过虚函数实现的,虚函数是类的成员函数,存在存储虚函数指针的表叫做虚函数表,虚函数表是一个存储类成员虚函数的指针,每个指针都指向调用它的地方,当子类调用虚函数时,就会去虚表里面找自己对应的函数指针,从而实现“谁调用、实现谁”从而实现多态即在系统编译的时候并不知道程序将要调用哪一个函数,只有在运行到这里的时候才能确定接下来会跳转到哪一个函数。
静态多态
又称编译期多态,即在系统编译期间就可以确定程序将要执行哪个函数,而静态多态则是通过函数重载(函数名相同,参数不同,两个函数在同一作用域),运算符重载,和重定义(又叫隐藏,指的是在继承关系中,子类实现了一个和父类名字一样的函数,(只关注函数名,和参数与返回值无关)这样的话子类的函数就把父类的同名函数隐藏了。隐藏只与函数名有关,与参数没有关系.)来实现的。
实现过程:
- 在类中用 virtual 关键字声明的函数叫做虚函数;
- 存在虚函数的类都有一个虚函数表,当创建一个该类的对象时,该对象有一个指向虚函数表的虚表指针(虚函数表和类对应的,虚表指针是和对象对应);
- 当基类指针指向派生类对象,基类指针调用虚函数时,基类指针指向派生类的虚表指针,由于该虚表指针指向派生类虚函数表,通过遍历虚表,寻找相应的虚函数。
class Base { public: virtual void fun() { cout << "Base::fun()" << endl; } virtual void fun1() { cout << "Base::fun1()" << endl; } virtual void fun2() { cout << "Base::fun2()" << endl; } }; class Son : public Base { public: void fun() { cout << "Son::Sonfun()" << endl; } virtual void Sonfun1() { cout << "Son::Sonfun1()" << endl; } virtual void Sonfun2() { cout << "Son::Sonfun2()" << endl; } }; int main() { Base* p = new Son(); p->fun(); // Son::fun() 调用派生类中的虚函数 return 0; }
new出来的对象是基类的,那么基类的虚函数表如下:
-
new出来的对象是基类的,那么基类的虚函数表如下:静态多肽和动态多肽:静态多肽 地址早绑定,在编译的时候就已经确定函数地址
# if 1 // 静态多肽 // 静态多肽 地址早绑定,在编译的时候就已经确定函数地址 class Animal { public: int m_A; void speak() { cout << "动物在说话" << endl; } }; class Cat:public Animal { public: int m_A; void speak() // 这里不是虚函数 { cout << "小猫在说话" << endl; } }; void doSpeak(Animal &animal) //如果是想要猫说话就不能让函数的地址早绑定 { animal.speak(); } void test1() // 静态多肽 { Cat cat; doSpeak(cat); } int main() { test1(); return 0; } #endif
动态多肽:在基类中写virtual关键字,调用的时候可以用父类的指针或者引用指向派生类对象
#if 1
// 动态多肽 父类的指针或者引用指向子类
class Animals {
public:
int m_A;
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Dog :public Animals
{
public:
void speak()
{
cout << "小狗在说话" << endl;
}
};
// 动态多肽: 父类的指针或者引用指向子类Animals& animals
void doSpeak2(Animals& animals) //如果是想要猫说话就不能让函数的地址早绑定
{
animals.speak();
}
int main()
{
Dog d;
doSpeak2(d);
return 0;
}
#endif
26、用宏实现比较大小,以及两个数中的最小值
27、什么是虚函数?什么是纯虚函数?以及区别,访问基类的私有虚函数?
C++虚函数表深入探索(详细全面) - 云+社区 - 腾讯云
虚函数:被 virtual 关键字修饰的成员函数,就是虚函数。多肽是通过虚函数来实现的。
int m_A;
virtual void speak()
{
cout << "动物在说话" << endl;
}
纯虚函数:
- 纯虚函数在类中声明时,加上 =0;
- 含有纯虚函数的类称为抽象类(只要含有纯虚函数这个类就是抽象类),类中只有接口,没有具体的实现方法;
- 继承纯虚函数的派生类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化对象。
# if 1 // 纯虚函数 和 抽象类
// 子类必须要重写父类中的虚方法
class Base
{
public:
virtual void func() = 0; // 纯虚函数
// 1 无法实例化
// 2 抽象类中的子类必须重写父类中的纯虚函数,不然这个子类也是抽象类。也是无法实例化的。
int n_Num1;
int n_Num2;
};
class Son :public Base
{
public :
virtual void func() {
cout<<" son" << endl;
}
};
void test01()
{
Base* base = new Son();
base->func();
}
int main()
{
test01();
}
#endif
访问基类的私有虚函数?
#if 1 // 访问基类的私有虚函数
class Base
{
private:
virtual void f0()
{
cout << "Base::f0()...." << endl;
}
virtual void f1()
{
cout << "Base::f1()...." << endl;
}
virtual void f2()
{
cout << "Base::f2()...." << endl;
}
};
class Son : public Base {};
typedef void(*Fun)(void);
int main(int argc, char* argv[])
{
Son s;
Fun pFun0 = (Fun) * ((int*)*(int*)(&s) + 0);
Fun pFun1 = (Fun) * ((int*)*(int*)(&s) + 1);
Fun pFun2 = (Fun) * ((int*)*(int*)(&s) + 2);
pFun0();
pFun1();
pFun2();
return 0;
}
#endif
虚函数和纯虚函数的区别?
- 虚函数和纯虚函数可以出现在同一个类中,该类称为抽象基类。(含有纯虚函数的类称为抽象基类)
- 虚函数可以直接使用,纯虚函数必须在派生类中实现后才能使用;
- 虚函数在定义时在普通函数的基础上加上 virtual 关键字,纯虚函数定义时除了加上virtual 关键字还需要加上 =0;
- 虚函数必须实现,否则编译器会报错;
- 对于实现纯虚函数的派生类,该纯虚函数在派生类中被称为虚函数,虚函数和纯虚函数都可以在派生类中重写;
- 析构函数最好定义为虚函数,特别是对于含有继承关系的类;析构函数可以定义为纯虚函数,此时,其所在的类为抽象基类,不能创建实例化对象
28、虚函数的实现机制
多肽是通过虚函数来实现的,虚函数的地址保存在虚函数表中,在类的对象所在的内存空间中,保存了指向虚函数表的指针(称为“虚表指针”),通过虚表指针可以找到类对应的虚函数表。虚函数表解决了基类和派生类的继承问题和类中成员函数的覆盖问题,当用基类的指针来操作一个派生类的时候,这张虚函数表就指明了实际应该调用的函数
虚函数表相关知识点:
- 虚函数表存放的内容:类的虚函数的地址。
- 虚函数表建立的时间:编译阶段,即程序的编译过程中会将虚函数的地址放在虚函数表中。
- 虚表指针保存的位置:虚表指针存放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数的偏移量。
注:虚函数表和类绑定,虚表指针和对象绑定。即类的不同的对象的虚函数表是一样的,但是每个对象都有自己的虚表指针,来指向类的虚函数表。
在C++中,虚函数的实现原理基于两个关键概念:虚函数表和虚函数指针
虚函数表:每个包含虚函数的类都会生成一个虚函数表,其中存储着该类中所有虚函数的地址。虚函数表是一个由指针构成的数组,每个指针指向一个虚函数的实现代码。
虚函数指针:在对象的内存布局中,编译器会添加一个额外的指针,称为虚函数指针或虚表指针。这个指针指向该对象对应的虚函数表,从而让程序能够动态的调用虚函数。
当一个基类指针或引用调用虚函数时,编译器会使用虚表指针来查找该对象对应的虚函数表,并根据函数在虚函数表中的位置来调用正确的虚函数。
在编译阶段生成,虚函数和普通函数一样存放在代码段,只是它的指针又存放在了虚表之中。
29、nullptr 比 NULL 优势
- NULL:预处理变量,是一个宏,它的值是 0,定义在头文件 中,即 #define NULL 0。
- nullptr:C++ 11 中的关键字,是一种特殊类型的字面值,可以被转换成任意其他类型
nullptr 的优势:
- 有类型,类型是 typdef decltype(nullptr) nullptr_t;,使用 nullptr 提高代码的健壮性。
- 函数重载:因为 NULL 本质上是 0,在函数调用过程中,若出现函数重载并且传递的实参是 NULL,可能会出现,不知和哪一个函数匹配的情况;但是传递实参 nullptr 就不会出现这种情况。
30、指针和引用的区别?
C语言的指针和引用和c++的有什么区别:
- 指针所指向的内存空间在程序运行过程中可以改变,而引用所绑定的对象一旦绑定就不能改变
- 指针本身在内存中占有内存空间,引用相当于变量的别名,在内存中不占内存空间
- 指针可以为空,但是引用必须绑定对象
- 指针可以有多级,但是引用只能一级
- int** p1; // 合法。指向指针的指针
int*& p2; // 合法。指向指针的引用
int&* p3; // 非法。指向引用的指针是非法的
int&& p4; // 非法。指向引用的引用是非法的
什么时候用指针。什么时候用引用?
在函数参数传递的时候,什么时候使用指针,什么时候使用引用?
需要返回函数内局部变量的内存的时候用指针。使用指针传参需要开辟内存,用完要记得释放指针,不然会内存泄漏。而返回局部变量的引用是没有意义的
对栈空间大小比较敏感(比如递归)的时候使用引用。使用引用传递不需要创建临时变量,开销要更小
类对象作为参数传递的时候使用引用,这是C++类对象传递的标准方式
是否初始化:指针可以不用初始化,引用必须初始化
性质不同:指针是一个变量,引用是对被引用的对象取一个别名
占用内存单元不同:指针有自己的空间地址,引用和被引用对象占同一个空间
31、函数指针和指针函数的区别
指针函数:
指针函数本质是一个函数,只不过该函数的返回值是一个指针。相对于普通函数而言,只是返回值是指针。
#if 1 // 指针函数 函数返回的是一个指针
#include <iostream>
using namespace std;
struct Type
{
int var1;
int var2;
};
Type* fun(int tmp1, int tmp2)
{
Type* t = new Type();
t->var1 = tmp1;
t->var2 = tmp2;
return t;
}
int main()
{
Type* p = fun(5, 6);
return 0;
}
#endif // 0
函数指针:
函数指针本质是一个指针变量,只不过这个指针指向一个函数。函数指针即指向函数的指针。
#if 1 // 是一个指针 指向的是一样函数 ,调用的时候可以用这个指针从而可以使用那个函数
#include <iostream>
using namespace std;
int fun1(int tmp1, int tmp2)
{
return tmp1 * tmp2;
}
int fun2(int tmp1, int tmp2)
{
return tmp1 / tmp2;
}
int main()
{
int (*fun)(int x, int y);
fun = fun1;
cout << fun(15, 5) << endl;
fun = fun2;
cout << fun(15, 5) << endl;
return 0;
}
/*
运行结果:
75
3
*/
#endif // 0
函数指针和指针函数的区别:
- 本质不同
1.指针函数本质是一个函数,其返回值为指针。
2.函数指针本质是一个指针变量,其指向一个函数。 - 定义形式不同
1.指针函数:int* fun(int tmp1, int tmp2); ,这里* 表示函数的返回值类型是指针类型。
2.函数指针:int (fun)(int tmp1, int tmp2);,这里 表示变量本身是指针类型。 - 用法不同
32、虚析构函数的作用
基类采用虚析构函数可以防止内存泄漏。比如下面的代码中,如果基类 A 中不是虚析构函数,则 B 的析构函数不会被调用,因此会造成内存泄漏。
class A
{
public:
A()
{
cout << "基类的构造" << endl;
}
~A()
{
cout << "基类的析构" << endl;
}
};
class B : public A
{
public:
B()
{
cout << "子类的构造" << endl;
// new
}
~B()
{
cout << "子类的析构" << endl;
// delete memory
}
};
int main(int argc, char* argv)
{
A* p = new B;// some operations // ...
delete p; // 由于基类中是虚析构,这里会先调用B的析构函数,然后调用A的析构函数
return 0;
}
但并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。
33、参数传递时,值传递、引用传递、指针传递的区别?
参数传递的三种方式:
- 值传递:形参是实参的拷贝,函数对形参的所有操作不会影响实参。
- 指针传递:本质上是值传递,只不过拷贝的是指针的值,拷贝之后,实参和形参是不同的指针,通过指针可以间接的访问指针所指向的对象,从而可以修改它所指对象的值。
- 引用传递:当形参是引用类型时,我们说它对应的实参被引用传递。
34、什么是模板?如何实现?
模板:创建类或者函数的蓝图或者公式,分为函数模板和类模板。
实现方式:模板定义以关键字 template 开始,后跟一个模板参数列表。
- 模板参数列表不能为空;
- 模板类型参数前必须使用关键字 class 或者 typename,在模板参数列表中这两个关键字含义相同,可互换使用
#if 1 // 模板 泛型编程
// 函数模板
template <class T>
void Swap(T &a, T &b)
{
T temp = a;
a = b;
b = temp;
}
template<class T>
void mysort(T arr[], int len) {
for (int i = 0; i < len; i++)
{
int max = i;
for (int j = 0; j < len; j++)
{
if (arr[max]<arr[j]) {
max = j;
}
}
if (max!=i)
{
Swap(arr[max],arr[i]);
}
}
}
template <class T>
void PrintfArr(T arr[],int len)
{
for (int i = 0; i < len; i++)
{
cout << "数据 : "<<arr[i] << endl;
}
}
void test01() {
char charArr[] = "bcdcfe";
int num = sizeof(charArr) / sizeof(char);
mysort(charArr,num);
PrintfArr(charArr,num);
}
void test02() {
int Arr[] = { 1,3,7,2,8,4 };
int num = sizeof(Arr) / sizeof(int);
mysort(Arr, num);
PrintfArr(Arr, num);
}
int main() {
test02();
cout << "============================== " << endl;
test01();
system("pause");
return 0;
}
#endif
#if 1 //类模板
// 函数模板
template <class NameType,class AgeType>
class Person {
public :
Person(NameType name, AgeType age)
{
this->m_Age = age;
this->m_Name = name;
}
NameType m_Name;
AgeType m_Age;
};
void test01() {
Person<string, int> p1("albert", 666);
cout << p1.m_Name << " , " << p1.m_Age << endl;
}
int main() {
//test02();
cout << "============================== " << endl;
test01();
system("pause");
return 0;
}
#endif
函数模板和类模板的区别 :
- 实例化方式不同:函数模板实例化由编译程序在处理函数调用时自动完成,类模板实例化需要在程序中显式指定。
- 实例化的结果不同:函数模板实例化后是一个函数,类模板实例化后是一个类。
- 默认参数:类模板在模板参数列表中可以有默认参数。
- 特化:函数模板只能全特化;而类模板可以全特化,也可以偏特化。
- 调用方式不同:函数模板可以隐式调用,也可以显式调用;类模板只能显式调用。
函数模板调用方式举例: - 类模板中成员函数在调用的时候才去创建
- 类模板的继承,必须要知道父类中的类型才能继承
# if 1 // 类模板的继承 template<class T> class Base { public : T m_T; }; class Son :public Base<int> // 必须要知道父类中的类型才能继承 { }; template <class T1,class T2> class Son2 :public Base<T2> { public : T1 m_T1; Son2() { cout << "T1 的类型是: " << typeid(T1).name() << endl; cout << "T2 的类型是: " << typeid(T2).name() << endl; } }; void test01() { Son2 <int, char>s2; } int main() { test01(); } #endif
35、include " " 和 <> 的区别
include<文件名> 和 #include"文件名" 的区别:
- 查找文件的位置:include<文件名>在标准库头文件所在的目录中查找,如果没有,再到当前源文件所在目录下查找;#include"文件名" 在当前源文件所在目录中进行查找,如果没有;再到系统目录中查找。
- 使用习惯:对于标准库中的头文件常用 include<文件名>,对于自己定义的头文件,常用 #include"文件名
35、什么时候生成默认构造函数(无参构造函数)?什么时候生成默认拷贝构造函数?什么是深拷贝?什么是浅拷贝?默认拷贝构造函数是哪种拷贝?什么时候用深拷贝?
-
没有任何构造函数时,编译器会自动生成默认构造函数(无参构造函数);当类没有拷贝构造函数时,会生成默认拷贝构造函数
-
深拷贝是指拷贝后对象的逻辑状态相同,而浅拷贝是指拷贝后对象的物理状态相同;
-
默认拷贝构造函数属于浅拷贝。
-
当系统中有成员指代了系统中的资源时,需要深拷贝。比如指向了动态内存空间,打开了外存中的文件或者使用了系统中的网络接口等。如果不进行深拷贝,比如动态内存空间,可能会出现多次被释放的问题。是否需要定义拷贝构造函数的原则是,是类是否有成员调用了系统资源,如果定义拷贝构造函数,一定是定义深拷贝,否则没有意义。
更多可以参考下面的代码,比较容易混淆的是赋值操作符,其实区分很简单,在出现等号的时候,如果有构造新的对象时调用的就是构造,不然就是赋值操作符。 -
//构造函数: 可有参数 可以重载
//析构函数: 没有参数,不可以重载 -
C++ 的空类有哪些成员函数
//任何一个类的是三个方法 默认构造 默认析构 默认拷贝构造
// 构造函数的调用规则如下:
// 如果用户自定义有一个有参数构造,那么久不会在提供无参构造,但是会提供默认拷贝构造
// 如果用户自定义了拷贝构造,那么久不会提供其他构造函数了# if 1 // 深拷贝与浅拷贝 // 对于浅拷贝的问题可以自己实现一个拷贝构造来实现 class Person { public: string m_Name; int * score; Person() // m默认构造函数,不要用括号发来调用默认的构造函数 { cout << "Person默认无参数构造" << endl; } Person(const Person &p ) // 拷贝构造函数 { m_Name = p.m_Name; // 将传入的人的属性拷贝到我的身上 this->score = new int(*p.score); cout << "深拷贝构造的名字是 :" << m_Name << endl; } Person(string name,int score) { this->m_Name = name; this->score = new int(score); cout << "Person 的有参数构造" << this->m_Name << endl; } ~Person() { // 将堆区的数据释放掉 if (score !=NULL) { delete score; score = NULL; } cout << "~Person的析构函数" << endl; } }; void test01() { Person p1("jimao",124); cout << "=====test01==p1.m_Name, : " << p1.m_Name << "==p1.socre, : " << *p1.score << endl; Person p2(p1); cout << "=====test01==p2.m_Name, : " << p2.m_Name << "==p2.socre, : " << *p2.score << endl; } int main() { test01(); return 0; } #endif
36、泛型编程如何实现?
泛型编程实现的基础:模板。模板是创建类或者函数的蓝图或者说公式,当时用一个 vector 这样的泛型,或者 find 这样的泛型函数时,编译时会转化为特定的类或者函数。
泛型编程涉及到的知识点较广,例如:容器、迭代器、算法等都是泛型编程的实现实例。面试者可选择自己掌握比较扎实的一方面进行展开。
- 容器:涉及到 STL 中的容器,例如:vector、list、map 等,可选其中熟悉底层原理的容器进行展开讲解。
- 迭代器:在无需知道容器底层原理的情况下,遍历容器中的元素。
- 模板:可参考本章节中的模板相关问题
强制类型转换有哪几种?
static_cast:用于数据的强制类型转换,强制将一种数据类型转换为另一种数据类型。
1.用于基本数据类型的转换。
2.用于类层次之间的基类和派生类之间 指针或者引用 的转换(不要求必须包含虚函数,但必须是有相互联系的类),进行上行转换(派生类的指针或引用转换成基类表示)是安全的;进行下行转换(基类的指针或引用转换成派生类表示)由于没有动态类型检查,所以是不安全的,最好用 dynamic_cast 进行下行转换。
3.可以将空指针转化成目标类型的空指针。
4.可以将任何类型的表达式转化成 void 类型。
const_cast:强制去掉常量属性,不能用于去掉变量的常量性,只能用于去除指针或引用的常量性,将常量指针转化为非常量指针或者将常量引用转化为非常量引用(注意:表达式的类型和要转化的类型是相同的)。
reinterpret_cast:改变指针或引用的类型、将指针或引用转换为一个足够长度的整型、将整型转化为指针或引用类型。
dynamic_cast:
1.其他三种都是编译时完成的,动态类型转换是在程序运行时处理的,运行时会进行类型检查。
2.只能用于带有虚函数的基类或派生类的指针或者引用对象的转换,转换成功返回指向类型的指针或引用,转换失败返回 NULL;不能用于基本数据类型的转换。
3.在向上进行转换时,即派生类类的指针转换成基类类的指针和 static_cast 效果是一样的,(注意:这里只是改变了指针的类型,指针指向的对象的类型并未发生改变)。
4.在下行转换时,基类的指针类型转化为派生类类的指针类型,只有当要转换的指针指向的对象类型和转化以后的对象类型相同时,才会转化成功。
#if 1
#include <cstring>
using namespace std;
class Base
{
public:
virtual void fun()
{
cout << "Base::fun()" << endl;
}
};
class Derive : public Base
{
public:
virtual void fun()
{
cout << "Derive::fun()" << endl;
}
};
int main()
{
Base* p1 = new Derive();
Base* p2 = new Base();
Derive* p3 = new Derive();
//转换成功
p3 = dynamic_cast<Derive*>(p1);
if (p3 == NULL)
{
cout << "NULL" << endl;
}
else
{
cout << "NOT NULL" << endl; // 输出
}
//转换失败
p3 = dynamic_cast<Derive*>(p2);
if (p3 == NULL)
{
cout << "NULL" << endl; // 输出
}
else
{
cout << "NOT NULL" << endl;
}
return 0;
}
#endif // 0