C++面试宝典
Posted 苍山有雪,剑有霜
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++面试宝典相关的知识,希望对你有一定的参考价值。
这是当时春招实习面试的时候总结的C++面试笔记,拿了腾讯、美团、CVTE、阿里的offer,最后去了阿里实习,一共三万字,建议慢慢看
文章目录
- **编译过程**
- **C++特性**
- **C++11特性**
- **C++类**
- **继承与多态**
- **new、delete、malloc、free、堆、栈**
- **sizeof,strlen****以及内存对齐**
- **strcpy****、****strncpy****、****memset**
- **类型转换操作符**
- **STL标准库**
- **引用与指针**
- **两者的区别**
- **两者的区别2**
- **模板编程**
- **C++****相关面试题**
- **C++中为什么可以通过指针或引用实现多态,而不可以通过对象呢?**
- **全局数组(不在****main****函数内)和局部数组初始化变量?**
- **指向****const****常量的指针和****const****指针**
- **noexcept****修饰符**
- **如何阻止类被继承呢?**
- **请你说说C语言参数压栈顺序?**
- **内存池的实现**
- **operator new和new?**
- **请介绍线程池,简述构造原理?可以手写一个简单的线程池么?**
- **explicit关键词有什么作用?**
- **连等?||和&&?strlen和sizeof?**
- **知道断言和异常机制么?**
- **类中成员默认内联?如果类成员是引用(reference)?const和constexpr?**
- **array、vector、数组的区别?sizeof不一样?**
- **auto了解嘛?**
- **delete []**
- **设计模式**
- **多线程**
- **兴趣阅读**
编译过程
编译链接
1.预处理器
C/C++的预处理器其实就是一个词法(而不是语法)预处理器,其主要完成文本替换、宏展开以及删除注释等,完成这些操作之后,将会获得真正地**“源代码”**。
常见的include语句即是一个预处理器命名,在预处理器中它将所有的头文件包含进来。(该步骤的文件扩展名为****.i)
2.编译器
在这一步骤,将.i文件翻译为.s**,得到**汇编程序语言,值得注意的是所有的编译器输出的汇编语言都是同一种语法。
注:内联函数就是在这一环节“膨胀”进源码的,它的作用即在于:不是在调用时发生控制转移,而是在编译时将函数体嵌入在每一个调用处,适用于功能简单,规模较小又使用频繁的函数。递归函数无法内联处理,内联函数不能有循环体,switch语句,不能进行异常接口声明。仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
3.汇编器
将**.s****翻译成机器语言指令**,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件.o中(把汇编语言翻译成机器语言的过程)。
4.链接器
链接(ld):gcc会到系统默认的搜索路径”/usr/lib”下进行查找,也就是链接到libc.so.6库函数中去。
函数库一般分为静态库和动态库两种。静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,**但在运行时也就不再需要库文件了。**其后缀名一般为”.a”。动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,**而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。**动态库一般后缀名为”.so”,如前面所述的libc.so.6就是动态库。gcc在编译时默认使用动态库。
https://www.jianshu.com/p/1bab86143f1c
动态/静态链接的区别
1、静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib 中的指令都全部被直接包含在最终生成的 EXE 文件中了,所以程序运行的时候不再需要其它的库文件。
但是若使用 DLL,该 DLL 不必被包含在最终 EXE 文件中,EXE 文件执行时可以**“动态”地引用和卸载这个与 EXE 独立的 DLL 文件**。静态链接库和动态链接库的另外一个区别在于静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。
2、动态库就是在运行时需要调用其中的函数时,根据函数映射表找到该函数然后调入堆栈执行。如果在当前工程中有多处对dll文件中同一个函数的调用,那么执行时,这个函数只会留下一份拷贝。但是如果有多处对lib文件中同一个函数的调用,那么执行时,该函数将在当前程序的执行空间里留下多份拷贝,而且是一处调用就产生一份拷贝。
静态链接的优缺点
静态链接的缺点很明显,一是浪费空间,因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,如多个程序中都调用了printf()函数,则这多个程序中都含有printf.o,所以同一个目标文件都在内存存在多个副本;另一方面就是更新比较困难,因为每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。
为什么会出现动态链接
动态链接出现的原因就是为了解决静态链接中提到的两个问题,一方面是空间浪费,另外一方面是更新困难。下面介绍一下如何解决这两个问题。
动态链接的优缺点
动态链接的优点显而易见,就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本;另一个优点是,更新也比较方便,更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。但是动态链接也是有缺点的,因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。
头文件、源文件、声明定义
源于一个问题:C++编译模式是怎样的?
简单来说就是**“事先声明”、“分别编译”、“事后链接”**的编译模式。
程序源代码无非就是变量和函数的总和,多个分开的文件分别编译自己的部分,在最后阶段进行互相链接,从而得到了一个可执行文件。
这是如何实现的呢?这就的提到声明和定义的区别。
简单地说,**“定义”就是将一个符号(函数和变量都是符号)完整描述:类型、参数、实现细节等;**而“声明”则简单很多:它只是“说明”有这样一个变量或者函数,但其内部是什么情况,**请等链接的时候我们再去找找。**从这就可以看出:一个函数或者变量可以被声明很多次,但是只能被定义一次!(这其实也就是头文件的好处,你想用这个函数,那就把我这个头文件给包含吧,最后链接的时候从其他目标文件去找就行了)
**到这又不得不提一点了,**头文件到底是个什么东西?
头文件其实跟源文件没什么区别,都是C++的源代码(因为在编译的时候头文件中的内容会被直接copy进cpp文件,但是有的时候头文件会互相包含,这可能就会造成在一份源码中copy两次同样的头文件,这也是为什么需要ifndef endif 或者#pragma once的用处*)。
PS:头文件相互包含总会有一个文件在另一个文件中被忽略。因为预处理时include是将包含的文件中的代码插入到当前代码里,文件是不能包含自己的**,如果相互包含编译器只能取舍一下,否则是不可能正常通过的。如果遇到这种情况就需要对头文件进行重构,修改其包含关系;
所以头文件里最好只放变量和函数的声明,而不能放它们的定义(如果多个函数都include定义,那么就会出错了)。
但!有三个例外!
其一,就是const/static可以在头文件的中定义,因为const/static默认为全局数据区,仅在当前文件有效,即使被多个文件包含也只会定义一次。
其二,就是内联函数的定义。内联函数和普通函数的区别在于编译阶段编译器需要知道内联函数的内部具体实现(才能够将其展开插入源代码),因此将内联函数放于头文件甚至有好处的。
其三,就是类的定义。程序在创建一个类对象的时候,编译器只有在类定义完全可见的情况下才能够对其进行布局(如内存分配、数据成员有哪些、函数接口有哪些),且也可以将函数成员的实现也放在头文件中,因为如果函数成员在类的定义体中被定义,那么就默认这个函数是内联的。
其实.cpp和.h文件名称没有任何直接关系,很多编译器都可以接受其他扩展名。
Extern有什么作用?
主要有两个功能:
一是,调用其他文件中外部定义的变量或函数(防止重定义);
二是,在C++中调用C语言代码,则编译器在编译fun这个函数名时按C的规则去翻译相应的函数名而不是C++的;
PS:extern “c” {}说明C++编译跟C编译有什么区别呢?
作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:
void foo( int x, int y );
该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像**_foo_int_int之类的名字**(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled name”)。
_foo_int_int这样的名字包含了函数名、函数参数数量及类型信息****,C++就是靠这种机制来实现函数重载的。例如,在C++中,函数void foo( int x, int y )与void foo( int x, float y )编译生成的符号是不相同的,后者为_foo_int_float。
同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以"."来区分。而本质上,编译器在进行编译时,与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。
一个全局变量的作用域默认是整个程序, 加了****static (即便加了extern,还是只能当前文件)或者加了 const(因为本身就是常量,无影响) 则是这个源文件。
如果在多个源文件 包含同一个名字的全局变量的定义,就会引起重定义。
因此要想在多个文件共用一个全局变量,我们只需在一个头文件里
声明(注意是声明)这个变量:extern int i;
然后在其中一个源文件只能是一个(通常就是头文件的同名源文件)里面 定义这个变量 int i =1**;**
注意不能用 i =1**;** 要用 int i =1**;**
最后在要使用这个变量的源文件(即其他源文件)里**#include** 头文件即可。
Static的特性?
static修饰的变量有一个重要特点那就是:该变量限制在该源文件内;
如果将static定义在头文件中,多个程序同时包含该头文件,那么在编译的时候就会产生多个同名变量,且互不影响,所以可以是可以但不建议。如果想实现其他文件访问该变量,可以使用extern的方式;
变量的定义一般不放在头文件里,但可以把声明放在头文件里,供其他文件引用这个变量。
比如:在test.c文件中定义变量static int global = 0;
可以在头文件test.h中声明这个变量为:extern int global;
要使用这个变量的其他文件,只要包含test.h就可以了。
参考链接:
https://bbs.csdn.net/topics/80055779
https://www.cnblogs.com/lulululu/p/3693865.html
C++特性
面向对象的理解?
面向对象就是通过将需求要素转化为对象进行问题处理的一种思想。C++面向对象的特性可以总结为:封装、继承和多态。
封装:
封装就是将程序模块化,对象化,把具体事物的特性属性和通过这些属性来实现一些动作的具体方法放在一个类中。对象是封装的最基本单位。
继承:
继承是子类自动共享父类数据和方法的机制。父类的相关属性,可以被子类重复使用,而对于子类中需要用到的新的属性和方法,子类可以自己扩展。
多态:
多态包含了重载和重写。
全局/局部静态变量?
静态变量都存放于全局数据区,都在程序退出时才销毁,两者唯一的区别就在于作用域不同,全局变量全局可见,而局部静态变量仅在局部区域可见。
作用域和生命周期是从两个不同的角度:时间和空间对变量进行描述。
作用域,即是该变量可被引用的范围;
生命周期即是该变量从初始化到销毁的时间;
一个程序的内存分为代码区、全局数据区、堆区、栈区,不同的内存区域,对应不同的生命周期。
函数指针与指针函数
函数指针是指向函数的指针,确切的说,是指向特定类型函数的指针(函数与函数指针 类型要匹配)
函数指针用来保存函数首地址,即可以通过该指针访问函数。函数指针相当于取别名。
函数指针可以指向一类函数,而不是一个函数,即可以重新赋值。
简单使用:
进阶使用:利用****typedef
typedef 返回类型(*新类型)(参数表)
指针函数是返回值为指针的函数,所以我们在main()中调用它时可以用一个同类型的指针来接收。
指针函数可以用来解决众多问题,如返回多个值的问题**。(见****“函数返回多个值的方法”**那篇文章)(见后续)
指针函数比函数指针更经常用到(哦?),一定要学会用
https://www.cnblogs.com/anwcq/p/c_zhizhenhanshu.html
这博客的例三很经典,涉及到了指针,数组指针,指针函数,二维数组的赋值,函数返回多个值,数组指针的自增与指针自增的区别。
***指针与数组
int (*p)[4] = a;//定义一个指向a的指针变量p
括号中的*表明 p 是一个指针,它指向一个数组,数组的类型为int [4],这正是 a 所包含的每个一维数组的类型。
[]的优先级高于*,( )是必须要加的,如果赤裸裸地写作int *p[4],那么应该理解为int *(p[4]),p 就成了一个指针数组,而不是二维数组指针。
p指向数组 a 的开头,也即第 0 行;p+1前进一行,指向第 1 行。
参考链接:http://c.biancheng.net/view/2022.html
PS:如何获取函数多个返回值?
法一、需要的变量在函数外定义,利用引用传值在函数内修改;
法二、将需要的值打包为数组(相同数据类型)、或结构体(不同的类型),返回指针;
迭代器 ++it,it++的源码
- 前置返回一个引用,后置返回一个对象
// ++i实现代码为:
int& operator++()
{
*this += 1;
return *this;
}
- 前置不会产生临时对象,后置必须产生临时对象,临时对象会导致效率降低
//i++实现代码为:
int operator++(int)
{
int temp = *this;
++*this;
return temp;
}
swith语句
1、switch语句中如果case语句后没加break,就会一次执行下去。
2、switch语句的各常量表达式的值不能相同,但次序不影响执行结果(可以把default放最前面)。
C/C++类型安全?
类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没被授权的内存区域。
C语言的类型安全
C只在局部上下文中表现出类型安全,比如试图从一种结构体的指针转换成另一种结构体的指针时,编译器将会报告错误,除非使用显式类型转换。
C++的类型安全
如果C++使用得当,它将远比C更有类型安全性。相比于C,C++提供了一些新的机制保障类型安全:
(1)操作符new返回的指针类型严格与对象匹配,而不是void*;
(2)C中很多以void*为参数的函数可以改写为C++模板函数,而模板是支持类型检查的;
(3)引入const关键字代替#define constants,它是有类型、有作用域的,而#define constants只是简单的文本替换;
(4)一些**#define宏可被改写为inline函数**,结合函数的重载,可在类型安全的前提下支持多种类型,当然改写为模板也能保证类型安全;
(5)C++提供了dynamic_cast关键字,使得转换过程更加安全,因为dynamic_cast比static_cast涉及更多具体的类型检查。
C++11特性
左值引用和右值引用**?**
在C++****中可以取地址的、有名字的就是左值(内存中),反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)(寄存器中)。
左值引用就是对一个左值进行引用的类型。右值引用就是对一个右值进行引用的类型。左值引用是具名变量的别名,右值引用则是不具名变量的别名。
https://blog.csdn.net/qianyayun19921028/article/details/80875002
还没懂?
对左值和右值的一个最常见的误解是:等号左边的就是左值,等号右边的就是右值。左值和右值都是针对表达式而言的,左值是指表达式结束后依然存在的持久对象,右值是指表达式结束时就不再存在的临时对象。
一个区分左值与右值的便捷方法是:看能不能对表达式取地址,如果能,则为左值,否则为右值。
在C++11中,区别表达式是左值或右值可以做这样的总结:当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
这就是完美转发**😗*主要用作模板函数的参数类型推导
https://www.cnblogs.com/Braveliu/p/12235618.html
**std::move()****:**可以将左值强制转换为右值;(右值转左值?将右值赋值给一个新的变量即可)
那么为什么需要右值引用这个东西呢**?**
可以从寄存器直接取值,降低性能损耗。
**T&&就是C++11****之后的右值引用(看到它就是右值!)(一般用作转移构造函数),**如:int &&a = i+j;
**T&**是普通的左值引用;
新特性总览?
1.long long ,nullptr(解除二义性),auto(编译时确定)
2.decltype():取出变量类型
3.constexptr等价于const
4.正则表达式(描述了一种字符串匹配的模式)
5.封装了多线程库如:mutex
6.for(auto d:v)
7.begin(v),end(v)
8.智能指针方面
9.可变参数模板:它对参数进行了高度泛化,它能表示0到任意个数、任意类型的参数。
\\10. 可变模版参数类:template< class… Types >class tuple;
11.右值引用:int &&i=10;将10这个值存到临时存储位置,再将临时存储位置的地址存给i
12.lambda:匿名函数:
auto func = [] () { cout << “hello,world”; };
func();
其中:[]函数开始,(填写函数参数),{函数主体};
13.新增容器
std::forward_list,和 std::list 的双向链表的实现不同,std::forward_list 使用单向链表进行实现,提供了 O(1) 复杂度的元素插入,不支持快速随机访问(这也是链表的特点),也是标准库容器中唯一一个不提供 size() 方法的容器。
当不需要双向迭代时,具有比 std::list 更高的空间利用率。
无序容器
std::unordered_map/std::unordered_multimap和 std::unordered_set/std::unordered_multiset。
底层都是hash表。
元组 std::tuple
std::array与数组
std::array 保存在栈内存中,相比堆内存(vector对象在栈内存,其中有指向堆内存的指针)中的std::vector。
先说说内置数组:
int a[5];
int *b = new int[5];
前者时建立在栈内存,后者是建立在堆内存。
既然有了内置的数组,为什么还要引入array呢?
内置的数组有很多麻烦的地方,比如无法直接对象赋值,无**法直接拷贝(比如两个数组无法直接赋值)**等等,同时内置的数组又有很多比较难理解的地方,比如数组名是数组的起始地址等等。
简单来说,std::array除了有内置数组支持随机访问、效率高、存储大小固定等特点外,还支持迭代器访问、获取容量、获得原始指针等高级功能。而且它还不会退化成指针给开发人员造成困惑。
PS:定义array时,可以使用{}来直接初始化,也可以使用另外的array来构造,但不可以使用内置数组来构造。
#include <iostream>
#include <array>
int main(int argc, char const *argv[])
{
std::array<int, 5> a0 = {0, 1, 2, 3, 4}; //正确
std::array<int, 5> a1 = a0; //正确
int m = 5;
int b[m]; //正确,内置数组
std::array<int, 5> a2; //正确
std::array<int, m> a3; //错误,array不可以用变量指定
std::array<int, 5> a4 = b; //错误,array不可以用数组指定
return 0;
}
他的一些内置函数参见:https://blog.csdn.net/qq_38410730/article/details/102802239
memory_order和atomic**?**
std::memory_order(可译为内存序,访存顺序)。
C++11 中规定了 **6 中访存次序(Memory Order)😗*只是针对原子变量的原子操作来说的!
建议阅读: 知乎 https://www.zhihu.com/question/24301047/answer/85844428
enum memory_order {
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst
};
上述内存序分为3类,顺序一致性模型(std::memory_order_seq_cst),Acquire-Release 模型(std::memory_order_consume, std::memory_order_acquire, std::memory_order_release, std::memory_order_acq_rel,)(获取/释放语义模型)和 Relax 模型(std::memory_order_relaxed)(宽松的内存序列化模型)。
memory_order_relaxed: 只保证当前操作的原子性,**不考虑线程间的同步,**其他线程可能读到新值,也可能读到旧值。
memory_order_release:(可以理解为 mutex 的 unlock 操作)
memory_order_acquire: (可以理解为 mutex 的 lock 操作)
memory_order_acq_rel:
对读取和写入施加 acquire-release 语义,无法被重排
可以看见其他线程施加 release 语义的所有写入,同时自己的 release 结束后所有写入对其他施加 acquire 语义的线程可见
memory_order_seq_cst:(顺序一致性)
如果是读取就是 acquire 语义,如果是写入就是 release 语义,如果是读取+写入就是 acquire-release 语义
同时会对所有使用此 memory order 的原子操作进行同步,所有线程看到的内存操作的顺序都是一样的,就像单个线程在执行所有线程的指令一样
通常情况下,默认使用 memory_order_seq_cst,所以你如果不确定怎么这些 memory order,就用这个。
std::atomic_flag是一个原子的布尔类型,可支持两种原子操作:
test_and_set, 如果atomic_flag对象被设置,则返回true; 如果atomic_flag对象未被设置,则设置之,返回false
clear. 清除atomic_flag对象
使用atomic_flag可实现mutex。
std::atomic对int, char, bool等数据结构进行原子性封装,在多线程环境中,对std::atomic对象的访问不会造成竞争-冒险。利用std::atomic可实现数据结构的无锁设计。
https://www.cnblogs.com/taiyang-li/p/5914331.html
C++类
**C++**的内存布局
所谓内存布局,即是研究以下的问题:
1* 类如何布局?
兼容C的struct:按照声明顺序对齐;
C++类的实例大小完全取决于其自身及基类的成员变量,成员函数不影响。
具体解释:https://blog.csdn.net/alidada_blog/article/details/81262152数据成员每一个类对象不同空间,但是函数是公用一份。
2* 成员变量如何访问?
3* 成员函数如何访问?
4* 所谓的“调整块”(adjuster thunk)是怎么回事?
5* 使用如下机制时,开销如何:
* 单继承、多重继承、虚继承
* 虚函数调用
* 强制转换到基类,或者强制转换到虚基类
* 异常处理
在计算大小(sizeof)的时候:
除了虚函数表指针、成员变量(非静态和常量),其他****的常量,静态,成员函数都不占类内存大小的。
参考以下博客,重要性依次降低:
这里有一道关于内存布局的题,贼妙!
https://blog.csdn.net/laozhong110/article/details/6402574
之所以拷贝到**&pca****,是将该指针自身的地址更改为CA的对象的地址;**
https://blog.twofei.com/496/
https://blog.csdn.net/fairyroad/article/details/6376620
https://www.cnblogs.com/noryes/p/6434245.html
https://www.cnblogs.com/QG-whz/p/4909359.html
https://www.cnblogs.com/mysky007/p/11042294.html
C++的内存分段
.txt(代码段) .data(全局静态已初始化变量) .bss(全局未初始化变量) heap(堆) stack(栈)
内存分段见下图:
https://blog.csdn.net/jirryzhang/article/details/79518408
友元类有什么用?
让类的函数或者其他类能够访问该类的内部成员变量、函数();
右元在类内进行声明(无关public和private,它不是成员函数),在类外进行定义;
在内类声明的原因就在于,为了声明这个函数可以访问该类的私有成员。
https://blog.csdn.net/yiwanyuan2756/article/details/80437536
重载、重写和覆盖?
重载:同名函数参数(包括参数类型,个数与顺序)或返回值不同,注意返回值不能作为重载的标志。
覆盖:派生类覆盖基类函数(虚函数)
重写:派生类重写基类函数,但不覆盖(基类函数是虚函数的话就变成覆盖了)
重载和覆盖有何不同?
虚函数总是在派生类被改写,这种改写被称为“override”(覆盖)。
https://www.cnblogs.com/zbzb1/p/11527983.html
析构函数中调用delete this?
会导致堆栈溢出。原因很简单,delete的本质是“为将被释放的内存调用一个或多个析构函数,然后,释放内存”。
显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。
构造函数作用?
类对象被创建时,完成系统内存空间分配,并自动调用该构造函数->由构造函数完成成员的初始化工作。
析构函数的作用则恰恰相反,调用成员的析构函数,释放类对象的内存。
只在堆创建或只在栈创建类?
只在堆创建的基本思路:将构造或析构改成私有;
只在栈:将operator new delete重载私有;
1、在栈上分配就是把new、delete运算符重载为private属性。只有使用new运算符,对象才会被建立在堆上,因此只要限制new运算符就可以实现类对象只能建立在栈上。可以将new运算符重载为私有。
2、动态建立一个类对象,就是使用new运算符为对象在堆空间中分配内存。
在堆上分配就是把构造、析构函数设为protected属性(时不能在类外直接访问构造函数),再用子类来动态创建对象或提供一个public的static函数来完成构造。)
以下这个博客的重点在于,C++内存机制的探讨:
https://blog.csdn.net/g5dsk/article/details/4775144
这是才是正解:
https://www.cnblogs.com/raichen/p/5808766.html
静态成员函数的特点?
1、被类的所有的对象共有,不属于某一个对象。通过类名::就可以直接调用。
2、跟普通的成员函数比,没有隐藏的this指针作为参数。这一点可用于封装线程类。
3、静态成员函数只可以访问静态成员变量。
静态成员变量要在类外初始化呢?
答:因为静态变量在main之前就已经在全局数据段产生的,它不应该去依赖类对象的生命周期。若是在类内初始化,说明需要等待该类实例化才初始化。因此在类外初始化,程序编译时就已完成。
https://www.cnblogs.com/sggggr/p/13570280.html
类的静态成员与普通成员的区别
生命周期
静态成员变量从类被加载开始到类被卸载,一直存在;
普通成员变量只有在类创建对象后才开始存在,对象结束,它的生命期结束;
共享方式
静态成员变量是全类共享;普通成员变量是每个对象单独享用的;
定义位置
普通成员变量存储在栈或堆中,而静态成员变量存储在静态全局区;
初始化位置
普通成员变量在类中初始化;静态成员变量在类外初始化;
设计一个类计算对象的个数?
声明静态成员变量count,类内声明,类外初始化;
构造函数、拷贝、赋值,变量+1;析构变量-1;
空类会添加哪些东西?
拷贝(缺省、拷贝、赋值)、析构;
Empty(); // 缺省构造函数//
Empty( const Empty& ); // 拷贝构造函数//
~Empty(); // 析构函数//
Empty& operator=( const Empty& ); // 赋值运算符//
成员列表初始化效率高?
对于在函数体中初始化,是在所有的数据成员被分配内存空间(并初始化)后才进行的。列表初始化是给数据成员分配内存空间时就进行初始化。
前者是初始化、赋值;后者是初始化;少了一个步骤,当然更快;
类成员变量初始化时按照类中声明的顺序初始化的,而不是按照初始化列表的排序方式。
拷贝(复制)构造
1、当类的数据成员中有指针类型,或存在动态内存分配时,默认的拷贝构造函数实现的只能是浅拷贝,浅拷贝会带来数据安全方面的隐患(如同一块内存的多次析构,任何一方变动都会影响到另一方)。
此时要实现正确的拷贝也就是深拷贝,必须自行编写拷贝构造函数。
2、注意:拷贝构造函数必须形式必须是:类名(类名&对象名),缺少&编译不通过(堆栈溢出)(因为重复套娃)
3、拷贝构造在三种情况下会被调用:
①用类的一个对象去初始化类的另一个新创建的对象;
②函数的形参是类对象,调用函数时(所以拷贝构造形参必须是引用类 型);
③函数的返回值是类的对象,函数执行完返回调用时(所以赋值函数用于连续赋值场合时返回值必须是引用类型);
4、A a = 10;
注意当A只有一个成员变量的时候是允许这么定义类的实例的。
运算符重载
- 注意运算符重载的形式和一般函数的形式非常类似,唯一区别就是将函数名换成operator 及 运算符两部分,如:
CMyString& operator = (const CMyString& str);
2、那程序什么时候执行拷贝构造,什么时候执行的是运算符重载里的内容呢?
CMyString str2=str1; //执行的拷贝构造
CMyString str2(str1); //执行的拷贝构造
str2 = str1; //执行的运算符重载
浅拷贝和深拷贝
可能会出现得问题的情况主要是由于:存在指针和内存分配。
#include <iostream>
using namespace std;
class Student
{
private:
int num;
char *name;
public:
Student();
~Student();
};
Student::Student()
{
name = new char(20);
cout << "Student" << endl;
}
Student::~Student()
{
cout << "~Student " << (int)name << endl;
delete name;
name = NULL;
}
int main()
{
{// 花括号让s1和s2变成局部对象,方便测试
Student s1;
Student s2(s1);// 复制对象
}
system("pause");
return 0;
}
执行结果:调用一次构造函数,调用两次析构函数,两个对象的指针成员所指内存相同,这会导致什么问题呢?name指针被分配一次内存,但是程序结束时该内存却被释放了两次,会导致崩溃!
因此需要添加拷贝构造:
Student::Student(const Student &s)
{
name = new char(20);
memcpy(name, s.name, strlen(s.name));
cout << "copy Student" << endl;
}
https://blog.csdn.net/caoshangpa/article/details/79226270
写时拷贝
是在写的时候(即改变字符串的时候)才会真正的开辟空间拷贝(深拷贝),如果只是对数据的读时,只会对数据进行浅拷贝。
写时拷贝:引用计数器的浅拷贝,又称延时拷贝:写时拷贝技术是通过"引用计数"实现的,在分配空间的时候多分配4个字节,用来记录有多少个指针指向块空间,当有新的指针指向这块空间时,引用计数加一,当要释放这块空间时,引用计数减一(假装释放),直到引用计数减为0时才真的释放掉这块空间。当有的指针要改变这块空间的值时,再为这个指针分配自己的空间(注意这时引用计数的变化,旧的空间的引用计数减一,新分配的空间引用计数加一)。
其实我们对写时拷贝并不陌生,Linux fork和STL string是比较典型的写时拷贝应用,本文只讨论STL string的写时拷贝。
string类的实现必然有个char成员变量,用以存放string的内容,写时拷贝针对的对象就是这个char成员变量。通过赋值或拷贝构造类操作,不管派生多少份string“副本”,每个“副本”的char成员都是指向相同的地址,也就是共享同一块内存,直到某个“副本”执行string写操作时,才会触发写时拷贝,拷贝一份新的内存空间出来,然后在新空间上执行写操作。显然,那些只读的“副本”节省了内存分配的时间和空间。
优秀博客:https://blog.csdn.net/qiansg123/article/details/80128063
移动拷贝
移动构造函数
-
我们用对象a初始化对象b,后对象a我们就不在使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷;
-
拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制。浅层复制之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另一个指针的指向就不合法了。所以我们只要避免第一个指针释放空间就可以了。避免的方法就是将第一个指针(比如a->value)置为NULL,这样在调用析构函数的时候,由于有判断是否为NULL的语句,所以析构a的时候并不会回收a->value指向的空间;
-
移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用。意味着,移动构造函数的参数是一个右值或者将亡值的引用。也就是说,只用用一个右值,或者将亡值初始化另一个对象的时候,才会调用移动构造函数。而那个move语句,就是将一个左值变成一个将亡值。
继承与多态
函数接口调用原则
1、**就近调用原则:**派生类的某一个对象调用某个接口,若本派生类有该接口的话就调用自己的,如果没有该接口,就会存在一个就近调用原则,如果父辈存在相关接口则优先调用父辈接口,如果父辈也不存在相关接口则调用祖辈接口。
2、调用过程中若不是直接(通过类域名)调用某一虚函数则会调用派生类对该接口的重写,若是直接调用某虚函数(通过类域名),则调用的就是该虚函数。
3、基类对象调用虚函数,若派生类对该虚类有重写则会优先调用该接口的重写。这句话是错误的!!!
基类对象(或指向基类对象的指针)调用虚函数是只会调用自己的,指针把基类指针指向派生类对象时才会调用派生类的。
4、注意派生类对象地址(指针)可以直接赋给基类指针,而基类赋给派生类必须显示转化。(看这个类引用他的成员会不会出错来区分)
5、构造函数是从最初的基类开始构造的,各个类的同名变量没有形成覆盖,都是单独的变量。
6、在C++的类继承中,建立对象时,首先调用基类的构造函数(不管基类构造函数是否带参数),然后在调用下一个派生类的构造函数,依次类推;析构对象时,其顺序正好与构造相反;
7、注意区别虚函数继承与虚继承(多重继承中特有的概念,虚拟基类是为解决多重继承而出现的)。
如何阻止类被继承呢?
提示:增加中间层:虚继承、友元类;
https://www.cnblogs.com/wangpei0522/p/4460425.html
多态的实现原理?
子类中对父类的虚函数进行了重写,那么利用基类指针就可以实现子类的动态调用。
1,如果以一个基类指针指向一个衍生类对象(派生类对象),那么经由该指针只能访问基础类定义的函数(静态连接),如果是存在虚函数,那就可以动态连接。
2,如果以一个衍生类指针指向一个基础类对象,必须先做强制转型动作(explicit cast),给程序员带来困扰。(一般不会这么去定义)
3,如果基础类和衍生类定义了相同名称的成员函数,那么通过对象指针调用成员函数时,到底调用那个函数要根据指针的原型来确定,而不是根据指针实际指向的对象类型确定。(基类和派生类之间的同名函数会被后者覆盖,而不存在重载,但可以显式指定调用)
虚拟函数就是为了对“如果你以一个基础类指针指向一个衍生类对象,那么通过该指针,你只能访问基础类定义的成员函数”这条规则反其道而行之的设计。
虚函数表?
定义实例时会在构造函数中进行虚表的创建和虚表指针的初始化,每个对象调用的虚函数都是通过虚表指针来索引的。
虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类有3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数**,那么虚表中的地址就会改变,指向自身的虚函数实现**。如果派生类有自己的虚函数,那么虚表中就会添加该项。
派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。
做一道题?
请问一下代码块的输出是啥?
#include <iostream>
using namespace std;
class A
{
public:
virtual void foo()
{
cout << "A's foo()" << endl;
bar();
}
virtual void bar()
{
cout << "A's bar()" << endl;
}
};
class B: public A
{
public:
void foo()
{
cout << "B's foo()" << endl;
A::foo();
}
void bar()
{
cout << "B's bar()" << endl;
}
};
int main()
{
B bobj;
A *aptr = &bobj;
aptr->foo();
A aobj = *aptr; //转化为A类对象
aobj.foo();
}
先写下来再看答案:
https://blog.csdn.net/cxycj123/article/details/81700621
静态多态和动态多态?
动态多态其实就是上述虚函数表中所提到的基类指针运行期间选择函数;
静态多态则是编译期间实现的,一是通过函数重载,二是通过函数模板;
https://www.cnblogs.com/lizhenghn/p/3667681.html
虚函数表是在运行/还是编译时创建的?
答:虚函数表在编译的时候就确定了,而类对象的虚函数指针vptr是在运行阶段确定的,这是实现多态的关键。(虚函数表示类所共有的,有点类似于static变量,存在全局数据区/常量区)
析构函数定义为虚函数?
直接的讲,C++中基类采用virtual虚析构函数是为了防止内存泄漏。
具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定(基类指针被撤销时,会先调用派生类的析构函数,再调用基类的析构函数。),因而只会调用基类的析构函数,而不会调用派生类的析构函数。
那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。
虚机制是什么?
包括:虚函数表、虚函数、继承、多态、菱形继承、纯虚函数、虚继承。
虚函数表有什么内容?
http://c.biancheng.net/view/267.html
任何有虚函数的类及其派生类的对象都包含虚函数表(准确来说是虚函数表的地址,64位机即8个字节,且虚函数表位于对象指针的前八个字节)。
多态
以上是关于C++面试宝典的主要内容,如果未能解决你的问题,请参考以下文章