常见C++笔试面试题整理

c cc cpp hpp inl 这些后缀名都有什么区别?

C中:

头文件后缀名: .h

源文件后缀名: .c

C++中:

头文件后缀名: .h, .hpp, .hxx

源文件后缀名:.cpp, .cc, .cxx, .C .c++

只是个名称,我们自己知道哪些是同一个东西,能区别开c和c++就行了。

另外,.h和.hpp的区别是:*.h里面只有声明,没有实现,而*.hpp里声明实现都有,后者可以减少.cpp的数量,适合用来编写公用的开源库。

inl 文件是内联函数的源文件。内联函数通常在c++头文件中实现,但有的时候 内联函数较多 或者出于一些别的考虑(使头文件看起来更简洁等),往往会将这部分具体 定义的代码添加到INL文件中 ,然后 在该头文件的末尾将其用#include引入 。由此也可以看到inl文件的例外一个用法的影子—— 模板函数、模板类的定义代码的存放


gcc 和 g++的区别

简单来说,gcc与g++都是GNU(组织)的一个编译器。需要注意以下几点:

  1. gcc与g++都可以编译c代码与c++代码。但是:后缀为.c的,gcc把它当做C程序,而g++当做是C++程序;后缀为.cpp的,两者都会认为是C++程序。
  2. 编译阶段,g++会调用gcc,对于c++代码,两者是等价的,但是因为gcc命令不能自动和C++程序使用的库联接,所以通常用g++来完成链接。
  3. 编译可以用gcc/g++,而链接可以用g++或者gcc -lstdc++。因为gcc命令不能自动和C++程序使用的库联接(当然可以选择手动链接,使用命令如下),所以通常使用g++来完成联接。但在编译阶段,g++会自动调用gcc,二者等价。
gcc main.cpp -lstdc++


gcc编译的四个步骤, 以最简单的hello.c为例子
一步到位:gcc hello.c
这条命令隐含执行了
(1)预处理
(2)编译
(3)汇编
(4)链接
这里未指定输出文件,默认输出为a.out
gcc编译C源码有四个步骤:
预处理 ----> 编译 ----> 汇编 ----> 链接
现在我们就用gcc的命令选项来逐个剖析gcc过程。
1)预处理(Pre-processing)
在该阶段,编译器将C源代码中的包含的头文件如stdio.h添加进来
参数:”-E”
用法:gcc -E hello.c -o hello.i
作用:将hello.c预处理输出hello.i文件。
2)编译(Compiling)
第二步进行的是编译阶段,在这个阶段中,gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc把代码翻译成汇编语言。
参数:”-S”
用法:gcc –S hello.i –o hello.s
作用:将预处理输出文件hello.i汇编成hello.s文件。
3)汇编(Assembling)
汇编阶段是把编译阶段生成的”.s”文件转成二进制目标代码“.o”文件
参数:“-c”
用法:gcc –c hello.s –o hello.o
作用:将汇编输出文件hello.s编译输出hello.o文件。
4)链接(Link)
在成功编译之后,就进入了链接阶段。
用法:gcc hello.o –o hello
作用:将编译输出文件hello.o链接成最终可执行文件hello。
运行该可执行文件,出现正确的结果如下。
>>> ./hello
Hello World!

C++11包含大量的新特性: 包含lambda表达式,类型推导keyword : auto、decltype,和模板的大量改进。

decltype实际上有点像auto的反函数,auto能够让你声明一个变量。而decltype则能够从一个变量或表达式中得到类型

nullptr是为了解决原来C++中NULL的二义性问题而引进的一种新的类型,由于NULL实际上代表的是0,

简化的for循环,能够用于遍历数组、容器、string以及由begin和end函数定义的序列(即有Iterator),for (auto p : m)

lambda表达式,能够用于创建并定义匿名的函数对象,以简化编程工作。Lambda的语法例如以下: [函数对象參数](操作符重载函数參数)->返回值类型{函数体}

vector<int> iv{5, 4, 3, 2, 1};
int a = 2, b = 1;
for_each(iv.begin(), iv.end(), [b](int &x){cout<<(x + b)<<endl;}); // (1)
for_each(iv.begin(), iv.end(), [=](int &x){x *= (a + b);});     // (2)
for_each(iv.begin(), iv.end(), [=](int &x)->int{return x * (a + b);});// (3)
  • []内的參数指的是Lambda表达式能够取得的全局变量。(1)函数中的b就是指函数能够得到在Lambda表达式外的全局变量,假设在[]中传入=的话,即是能够取得全部的外部变量,如(2)和(3)Lambda表达式
  • ()内的參数是每次调用函数时传入的參数。
  • ->后加上的是Lambda表达式返回值的类型。如(3)中返回了一个int类型的变量

变长參数的模板, C++11中引入了变长參数模板,所以发明了新的数据类型:tuple,tuple是一个N元组。能够传入1个, 2个甚至多个不同类型的数据

auto t1 = make_tuple(1, 2.0, "C++ 11");
auto t2 = make_tuple(1, 2.0, "C++ 11", {1, 0, 2});

避免了从前的pair中嵌套pair的丑陋做法。使得代码更加整洁

更加优雅的初始化方法, 在引入C++11之前。仅仅有数组能使用初始化列表,其它容器想要使用初始化列表,仅仅能用下面方法:

int arr[3] = {1, 2, 3}
vector<int> v(arr, arr + 3);

在C++11中,我们能够使用下面语法来进行替换:

int arr[3]{1, 2, 3};
vector<int> iv{1, 2, 3};
map<int, string>{{1, "a"}, {2, "b"}};
string str{"Hello World"};


什么是智能指针?智能指针的原理

将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。

智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放,

智能指针就是一种栈上创建的对象,函数退出时会调用其析构函数,这个析构函数里面往往就是一堆计数之类的条件判断,如果达到某个条件,就把真正指针指向的空间给释放了。

注意事项:

不能将指针直接赋值给一个智能指针,一个是类,一个是指针。

常用的智能指针

智能指针在C++11版本之后提供,包含在头文件<memory>中,shared_ptr、unique_ptr、weak_ptr

1)std::auto_ptr,有很多问题。 不支持复制(拷贝构造函数)和赋值(operator =),但复制或赋值的时候不会提示出错。所以可能会造成程序崩溃,比如

auto_ptr<string> p1(new string ("auto") ; //#1
auto_ptr<string> p2;                                    //#2
p2 = p1;                                                        //#3

在语句#3中,p2接管string对象的所有权后,p1的所有权将被剥夺。前面说过,这是好事,可防止p1和p2的析构函数试图刪同—个对象;
但如果程序随后试图使用p1,这将是件坏事,因为p1不再指向有效的数据。如果再访问p1指向的内容则会导致程序崩溃。

auto_ptr是C++98提供的解决方案,C+11已将将其摒弃,摒弃auto_ptr的原因,一句话总结就是: 避免潜在的内存崩溃问题。

2) C++11引入的unique_ptr, 也不支持复制和赋值,但比auto_ptr好,直接赋值会编译出错。实在想赋值的话,需要使用:std::move。例如:

std::unique_ptr<int> p1(new int(5))          // #4
std::unique_ptr<int> p2 = p1; // 编译会出错         //#5
std::unique_ptr<int> p3 = std::move(p1); // 转移所有权, 现在那块内存归p3所有, p1成为无效的指针. //#6

编译器认为语句#5非法,因此,unique_ptr比auto_ptr更安全。

但unique_ptr还有更聪明的地方。 有时候,会将一个智能指针赋给另一个并不会留下危险的悬挂指针。当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做

unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1;                                      // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed

其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。 这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。

3) C++11或boost的shared_ptr,基于引用计数的智能指针。可随意赋值,直到内存的引用计数为0的时候这个内存会被释放。

4)C++11或boost的weak_ptr,弱引用。 引用计数有一个问题就是互相引用形成环,这样两个指针指向的内存都无法释放。需要手动打破循环引用或使用weak_ptr。顾名思义,weak_ptr是一个弱引用,只引用,不计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前需要检查weak_ptr是否为空指针。


智能指针的作用

C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,野指针,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。


1、C和C++的区别

1)C是面向过程的语言,是一个结构化的语言,考虑如何通过一个过程对输入进行处理得到输出;C++是面向对象的语言,主要特征是“封装、继承和多态”。封装隐藏了实现细节,使得代码模块化;派生类可以继承父类的数据和方法,扩展了已经存在的模块,实现了代码重用;多态则是“一个接口,多种实现”,通过派生类重写父类的虚函数,实现了接口的重用。

2)C和C++动态管理内存的方法不一样,C是使用malloc/free,而C++除此之外还有new/delete关键字。

3)C++支持函数重载,C不支持函数重载

4)C++中有引用,C中不存在引用的概念


2、C++中指针和引用的区别

1)指针是一个新的变量,存储了另一个变量的地址,我们可以通过访问这个地址来修改另一个变量;

引用只是一个别名,还是变量本身,对引用的任何操作就是对变量本身进行操作,以达到修改变量的目的

2)引用只有一级,而指针可以有多级

3)指针传参的时候,还是值传递,指针本身的值不可以修改,需要通过解引用才能对指向的对象进行操作

引用传参的时候,传进来的就是变量本身,因此变量可以被修改


3、结构体struct和共同体union(联合)的区别

结构体:将不同类型的数据组合成一个整体,是自定义类型

共同体:不同类型的几个变量共同占用一段内存

1)结构体中的每个成员都有自己独立的地址,它们是同时存在的;

共同体中的所有成员占用同一段内存,它们不能同时存在;

2)sizeof(struct)是内存对齐后所有成员长度的总和,sizeof(union)是内存对齐后最长数据成员的长度、

结构体为什么要内存对齐呢?

1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常

2.硬件原因:经过内存对齐之后,CPU的内存访问速度大大提升。


4、#define和const的区别

1)#define定义的常量没有类型,所给出的是一个立即数;const定义的常量有类型名字,存放在静态区域

2)处理阶段不同,#define定义的宏变量在预处理时进行替换,可能有多个拷贝,const所定义的变量在编译时确定其值,只有一个拷贝。

3)#define定义的常量是不可以用指针去指向,const定义的常量可以用指针去指向该常量的地址

4)#define可以定义简单的函数,const不可以定义函数


5、重载overload,覆盖(重写)override,隐藏(重定义)overwrite,这三者之间的区别

1)overload,将语义相近的几个函数用同一个名字表示,但是参数列表 (参数的类型,个数,顺序不同) 不同,这就是函数重载,返回值类型可以不同

特征:相同范围(同一个类中)、函数名字相同、参数不同、virtual关键字可有可无

2)override,派生类覆盖基类的虚函数,实现接口的重用,返回值类型必须相同

特征:不同范围(基类和派生类)、函数名字相同、参数相同、基类中必须有virtual关键字(必须是虚函数)

3)overwrite,派生类屏蔽了其同名的基类函数,返回值类型可以不同

特征:不同范围(基类和派生类)、函数名字相同、参数不同或者参数相同且无virtual关键字


6、new、delete、malloc、free之间的关系

new/delete,malloc/free都是动态分配内存的方式

1)malloc对开辟的空间大小严格指定,而new只需要对象名

2)new为对象分配空间时,调用对象的构造函数,delete调用对象的析构函数

既然有了malloc/free,C++中为什么还需要new/delete呢?

运算符是语言自身的特性,有固定的语义,编译器知道意味着什么,由编译器解释语义,生成相应的代码。

库函数是依赖于库的,一定程度上独立于语言的。编译器不关心库函数的作用,只保证编译,调用函数参数和返回值符合语法,生成call函数的代码。

malloc/free是库函数,new/delete是C++运算符。对于非内部数据类型而言,光用malloc/free无法满足动态对象都要求。new/delete是运算符,编译器保证调用构造和析构函数对对象进行初始化/析构。但是库函数malloc/free是库函数,不会执行构造/析构。


7、delete和delete[]的区别

delete只会调用一次析构函数,而delete[]会调用每个成员的析构函数

用new分配的内存用delete释放,用new[]分配的内存用delete[]释放


多态, 虚函数, 纯虚函数

多态:不同对象接收相同的消息产生不同的动作。多态包括 编译时多态 运行时多态

运行时多态是:通过继承和虚函数来体现的。
编译时多态:运算符重载上。
封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了——代码重用。多态也有代码重用的功能,还有解决项目中紧耦合的问题,提高程序的可扩展性。C++实现多态的机制很简单,在继承体系下,将父类的某个函数给成虚函数(即加上virtual关键字),在派生类中对这个虚函数进行重写,利用父类的指针或引用调用虚函数。通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。对于虚函数调用来说,每一个 对象 内部都有 一个 虚表指针,在构造子类对象时,执行构造函数中进行虚表的创建和虚表指针的初始化,该虚表指针被初始化为 本类的虚表 。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以呢,才能实现动态的对象函数调用,这就是C++多态性实现的原理。
需要注意的几点 总结 (基类有虚函数):
1、每一个类都有虚表,单继承的子类拥有一张虚表,子类对象拥有一个虚表指针;若子类是多重继承(同时 继承多个基类 ),则子类维护多张虚函数表(针对不同基类构建不同虚表),该子类的对象也将包含 多个虚表指针

2、虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。
3、派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。

第一:编译器在发现Father 类中有虚函数时,会自动为每个含有虚函数的类生成一份虚函数表,也叫做虚表,该表是一个一维数组,虚表里保存了虚函数的入口地址。

第二:编译器会在每个对象的前四个字节中保存一个虚表指针,即(vptr),指向对象所属类的虚表。在程序运行时的合适时机,根据对象的类型去初始化vptr,从而让vptr指向正确的虚表,从而在调用虚函数时,能找到正确的函数。

第三:所谓的合适时机,在派生类定义对象时,程序运行会自动调用构造函数,在构造函数中创建虚表并对虚表初始化。在构造子类对象时,会先调用父类的构造函数,此时,编译器只“看到了”父类,并为父类对象初始化虚表指针,令它指向父类的虚表;当调用子类的构造函数时,为子类对象初始化虚表指针,令它指向子类的虚表。


虚函数: 在基类中用virtual的成员函数。允许在派生类中对基类的虚函数重新定义。
基类的虚函数可以有函数体,基类也可以实例化。
虚函数要有函数体,否则编译过不去。
虚函数在子类中可以不覆盖。
构造函数不能是虚函数。

纯虚函数:基类中为其派生类保留一个名字,以便派生类根据需要进行定义。
包含一个纯虚函数的类是抽象类。
纯虚函数后面有 = 0;
抽象类不可以实例化。但可以定义指针。
如果派生类如果不是先基类的纯虚函数,则仍然是抽象类。
抽象类可以包含虚函数。


8、STL库用过吗?常见的STL容器有哪些?算法用过几个?

STL包括两部分内容:容器和算法

容器即存放数据的地方,比如array, vector,分为两类,序列式容器和关联式容器

序列式容器,其中的元素不一定有序,但是都可以被排序,比如vector,list,queue,stack,heap, priority-queue, slist

关联式容器,内部结构是一个平衡二叉树,每个元素都有一个键值和一个实值,比如map, set, hashtable, hash_set

算法有排序,复制等,以及各个容器特定的算法

迭代器是STL的精髓,迭代器提供了一种方法,使得它能够按照顺序访问某个容器所含的各个元素,但无需暴露该容器的内部结构,它将容器和算法分开,让二者独立设计。


9、const知道吗?解释一下其作用

const修饰类的成员变量,表示常量不可能被修改

const修饰类的成员函数,表示该函数不会修改类中的数据成员,不会调用其他非const的成员函数

const函数只能调用const函数,非const函数可以调用const函数


10、虚函数是怎么实现的

每一个含有虚函数的类都至少有有一个与之对应的虚函数表,其中存放着该类所有虚函数对应的函数指针(地址),

类的示例对象不包含虚函数表,只有虚指针;

派生类会生成一个兼容基类的虚函数表。


11、堆和栈的区别

1)栈 stack 存放函数的参数值、局部变量,由编译器自动分配释放

堆heap,是由new分配的内存块,由应用程序控制,需要程序员手动利用delete释放,如果没有,程序结束后,操作系统自动回收

2)因为堆的分配需要使用频繁的new/delete,造成内存空间的不连续,会有大量的碎片

3)堆的生长空间向上,地址越大,栈的生长空间向下,地址越小


12、关键字static的作用

1)函数体内: static 修饰的局部变量作用范围为该函数体,不同于auto变量,其内存只被分配一次,因此其值在下次调用的时候维持了上次的值

2)模块内:static修饰全局变量或全局函数,可以被模块内的所有函数访问,但是不能被模块外的其他函数访问,使用范围限制在声明它的模块内

3)类中:修饰成员变量,表示该变量属于整个类所有,对类的所有对象只有一份拷贝

4)类中:修饰成员函数,表示该函数属于整个类所有,不接受this指针,只能访问类中的static成员变量

注意和const的区别!!!const强调值不能被修改,而static强调唯一的拷贝,对所有类的对象


13、STL中map和set的原理(关联式容器)

map和set的底层实现主要通过红黑树来实现

红黑树是一种特殊的二叉查找树

1)每个节点或者是黑色,或者是红色

2)根节点是黑色

3) 每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]

4)如果一个节点是红色的,则它的子节点必须是黑色的

5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

特性4)5)决定了没有一条路径会比其他路径长出2倍,因此红黑树是接近平衡的二叉树。


14、#include<file.h> #include "file.h" 的区别

前者是从标准库路径寻找

后者是从当前工作路径


15、什么是内存泄漏?面对内存泄漏和指针越界,你有哪些方法?

动态分配内存所开辟的空间,在使用完毕后未手动释放,导致一直占据该内存,即为内存泄漏。

方法:malloc/free要配套,对指针赋值的时候应该注意被赋值的指针是否需要释放;使用的时候记得指针的长度,防止越界


16、定义和声明的区别

声明是告诉编译器变量的类型和名字,不会为变量分配空间

定义需要分配空间,同一个变量可以被声明多次,但是只能被定义一次


17、C++文件编译与执行的四个阶段

1)预处理:根据文件中的预处理指令来修改源文件的内容

2)编译:编译成汇编代码

3)汇编:把汇编代码翻译成目标机器指令

4)链接:链接目标代码生成可执行程序


18、STL中的vector的实现,是怎么扩容的?

vector使用的注意点及其原因,频繁对vector调用push_back()对性能的影响和原因。

vector就是一个动态增长的数组,里面有一个指针指向一片连续的空间,当空间装不下的时候,会申请一片更大的空间,将原来的数据拷贝过去,并释放原来的旧空间。当删除的时候空间并不会被释放,只是清空了里面的数据。对比array是静态空间一旦配置了就不能改变大小。

vector的动态增加大小的时候,并不是在原有的空间上持续新的空间(无法保证原空间的后面还有可供配置的空间),而是以原大小的两倍另外配置一块较大的空间,然后将原内容拷贝过来,并释放原空间。在VS下是1.5倍扩容,在GCC下是2倍扩容。

在原来空间不够存储新值时,每次调用push_back方法都会重新分配新的空间以满足新数据的添加操作。如果在程序中频繁进行这种操作,还是比较消耗性能的。


19、STL中unordered_map和map的区别

map是STL中的一个关联容器,提供键值对的数据管理。底层通过红黑树来实现,实际上是二叉排序树和非严格意义上的二叉平衡树。所以在map内部所有的数据都是有序的,且map的查询、插入、删除操作的时间复杂度都是O(logN)。

unordered_map和map类似,都是存储key-value对,可以通过key快速索引到value,不同的是unordered_map不会根据key进行排序。unordered_map底层是一个防冗余的哈希表,存储时根据key的hash值判断元素是否相同,即unoredered_map内部是无序的。


20、C++的内存管理

在C++中,内存被分成五个区:栈、堆、自由存储区、静态存储区、常量区

栈:存放函数的参数和局部变量,编译器自动分配和释放

堆:new关键字动态分配的内存,由程序员手动进行释放,否则程序结束后,由操作系统自动进行回收

自由存储区:由malloc分配的内存,和堆十分相似,由对应的free进行释放

全局/静态存储区:存放全局变量和静态变量

常量区:存放常量,不允许被修改


21、 构造函数为什么一般不定义为虚函数?而析构函数一般写成虚函数的原因 ?

1、构造函数不能声明为虚函数

1)因为创建一个对象时需要确定对象的类型,而虚函数是在运行时确定其类型的。而在构造一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型,是类本身还是类的派生类等等

2)虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数即构造函数了

2、析构函数最好声明为虚函数

首先析构函数可以为虚函数,当析构一个指向派生类的基类指针时,最好将基类的析构函数声明为虚函数,否则可以存在内存泄露的问题。

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


子类析构时,要调用父类的析构函数吗?

析构函数调用的次序时先派生类后基类的。和构造函数的执行顺序相反。并且析构函数要是virtual的,否则如果用父类的指针指向子类对象的时候,析构函数静态绑定,不会调用子类的析构。

不用显式调用,会自动调用


22、静态绑定和动态绑定的介绍

静态绑定和动态绑定是C++多态性的一种特性

1)对象的静态类型和动态类型

静态类型:对象在声明时采用的类型,在编译时确定

动态类型:当前对象所指的类型,在运行期决定,对象的动态类型可变,静态类型无法更改

2)静态绑定和动态绑定

静态绑定:绑定的是对象的静态类型,函数依赖于对象的静态类型,在编译期确定

动态绑定:绑定的是对象的动态类型,函数依赖于对象的动态类型,在运行期确定

只有虚函数才使用的是动态绑定,其他的全部是静态绑定


23、 引用是否能实现动态绑定,为什么引用可以实现

可以。因为引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指的对象的实际类型所定义的。


24、深拷贝和浅拷贝的区别

深拷贝和浅拷贝可以简单的理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,如果资源重新分配了就是深拷贝;反之没有重新分配资源,就是浅拷贝。


25、 什么情况下会调用拷贝构造函数(三种情况)

系统自动生成的构造函数:普通构造函数和拷贝构造函数 (在没有定义对应的构造函数的时候)

生成一个实例化的对象会调用一次普通构造函数,而用一个对象去实例化一个新的对象所调用的就是拷贝构造函数

调用拷贝构造函数的情形:

1)用类的一个对象去初始化另一个对象的时候

2)当函数的参数是类的对象时,就是值传递的时候,如果是引用传递则不会调用

3)当函数的返回值是类的对象或者引用的时候

举例:

#include <iostream>
#include <string>
using namespace std;
class A{
	private:
		int data;
	public:
		A(int i){ data = i;} 	//自定义的构造函数
		A(A && a);  			//拷贝构造函数 
		int getdata(){return data;} 
//拷贝构造函数 
A::A(A && a){
	data = a.data;
	cout <<"拷贝构造函数执行完毕"<<endl;
//参数是对象,值传递,调用拷贝构造函数
int getdata1(A a){
	return a.getdata();
//参数是引用,引用传递,不调用拷贝构造函数 
int getdata2(A &a){
	return a.getdata();
//返回值是对象类型,会调用拷贝构造函数
 A getA1(){
 	A a(0);
 	return a;
 //返回值是引用类型,会调用拷贝构造函数,因为函数体内生成的对象是临时的,离开函数就消失
 A& getA2(){
 	A a(0);
 	return a;
 int main(){
    A a1(1);  
    A b1(a1);           		//用a1初始化b1,调用拷贝构造函数  
    A c1=a1;            		//用a1初始化c1,调用拷贝构造函数