面试速通宝典——5
121. 简述多态实现的原理
- 编译器发现了一个类中有虚函数,便会立即为此类生成虚函数表vtable。
- 虚函数表的各表项为指向对应虚函数的指针。
- 编译器还会在此类中隐含插入一个指针vptr(对vc编译器来说,他插在类的第一个位置上)指向虚函数表。
- 调用此类的构造函数时,在类的构造函数中,编译器会隐含执行vptr与vtable的关联代码,将vptr指向对应的vtable,将类与此类的vtable联系了起来。
-
另外在调用类的构造函数时,**指向基础类的指针此时已经变成指向具体的类的this指针**,这样依靠此this指针即可得到正确的vtable。
-
如此才能真正的与函数体进行连接,**这就是动态联翩**,实现多态的基本原理。
解释:
多态性是面向对象编程的一个重要特性,允许不同类型的对象通过同一个接口表现出不同的行为。在C++中,多态性主要通过虚函数来实现,而虚函数表和vptr指针就是支持多态的关键。
那么这段话为何代表多态实现的原理呢?我们一步一步来解析:
-
虚函数表vtable:当一个类中定义了虚函数,编译器会为这个类生成一个虚函数表,表中的每一项都是不同虚函数的地址。这样,当我们通过指针或引用调用虚函数时,就能通过该表找到真正的函数体。
-
vptr指针:每个存在虚函数的类实例对象都会有一个vptr指针,它指向虚函数表。通过这个指针,我们能够在运行时获取到正确的虚函数表。
-
构造函数和vptr:当我们创建一个类的实例或某种类型的指针时,其中隐含的代码就会将vptr指针与对应的虚函数表关联起来。
-
this指针:在构造函数中,基类的指针会转换为指向真实类型的this指针。利用该指针,我们可以按照虚函数表的顺序找到正确的函数体,从而实现所谓的“动态联接”。
所以,当我们调用一个类实例的虚函数时,程序会根据对象的实际类型(也就是它的vptr指针)动态地找到并调用正确的函数体。这样,即便我们用基类的指针或引用指向不同的派生类对象,也能呈现出不同的行为,这就是多态实现的基本原理。
请注意:
一定要区分虚函数、纯虚函数、虚拟继承之间的关系和区别。牢记虚函数实现原理,因为多态C++面试的重要考点之一,而虚函数是实现多态的基础。
解释:
这段话的意思是在理解和掌握C++中的虚函数,纯虚函数,和虚拟继承概念是非常重要的,因为这些是C++多态性的核心。下面,我会解释每一个概念的含义并说明它们之间的关系:
-
虚函数:这是一个在基类中声明的函数,它可以在派生类中被重写(override)。当基类对象的指针或引用指向派生类对象时,会动态地调用覆盖的版本,这就是多态性的表现。这就是为什么说虚函数是实现多态的基础。
-
纯虚函数:这是一种特殊的虚函数,它在基类中被声明为 “= 0”,这表示这个函数没有默认实现,必须在任何派生类中重写。这样的基类通常被称为抽象类,不能被实例化。这与虚函数提供默认的实现并允许被覆盖是有区别的。
-
虚拟继承:虚拟继承是一种解决多重继承下的菱形继承问题的技术。在菱形继承中,底部的派生类会继承多个来自不同途径的同一个基类拷贝,如果不使用虚拟继承,就会产生二义性。而虚拟继承确保了来自多个路径的基类只有一份拷贝被继承,避免了这种二义性。
122. 链表和数组有什么区别
数组和链表有以下几点不同:
- 存储形式:数组是一块连续的空间,声明时就要确定长度。链表是一块可不连续的动态空间,长度可变,每个结点要保存相邻结点指针。
- 数据查找:数组的线性查找速度快,查找操作直接用偏移地址。链表需要按顺序检索结点,效率低。
- 数据插入或删除:链表可以快速插入和删除结点,而数组则可能需要大量数据移动。
- 越界问题:链表不存在越界问题,数组有越界问题.
请注意:
在选择数组或链表数据结构时,一定要根据实际需要进行选择,数组便于查询,链表便于插入删除。数组节省空间但是长度固定。链表虽然变长但是占用了更多的存储空间。
123. 简述队列和栈的异同
队列和栈都是线性存储结构,但是两者的插入和删除数据的操作不同,队列是“先进后出”,栈是“后进先出”。
请注意:
区别栈区和堆区。
堆区的存取是“顺序随意”,栈区是“后进先出”。
栈是由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
堆一般是由程序员分配释放,若程序员不释放,程序结束时可由OS回收。分配方式类似于链表。
它与本题中的堆和栈是两回事。堆栈只是一种数据结构,而堆区和栈区是程序的不同内存存储区域。
124. 谈谈你对编程规范的理解或认识
编程规范可总结为:程序的可行性、可读性、可移植性和可测试性。
请记住:
编程规范是指在编写代码时应遵守的一些准则和标准,主要是为了保证以下四个方面的要求:
-
可行性:你的代码需要能够顺利运行,完成你希望它完成的任务。一个可行的程序设计应有效地解决了你希望解决的问题,并做到了准确性和效率。
-
可读性:你的代码应当易于理解,别的程序员应该能够读懂你的代码并且理解它的工作原理。这包括了逻辑清晰,命名合理以及有充足的注释等。
-
可移植性:你的代码应该具有在不同的环境或平台运行的能力。例如,如果你的代码只能在特定的操作系统上运行,那么它不能被认为是可移植的。一个具有良好可移植性的代码应当对环境依赖最小,且易于在新环境下进行配置。
-
可测试性:你的代码应该方便进行测试,当代码进行修改或者出现问题时,可以容易地定位问题并进行修复。包括对单元测试,集成测试等都有良好支持,并且可以输出有利于理解的测试结果。
遵循这些规范,可以帮助我们编写出更高质量的代码,更易于维护和升级,提高了代码的工作效率和准确性,也降低了因误解和错误而导致的问题。
125. short i = 0 ; i = i + 1L;这两句有错么?
代码 2 是错的,代码 1 是正确的。
请记住:
在数据安全的情况下,大类型的数据向小类型的数据转换一定要显示的强制类型转换。
解释:
这段话涉及的是编程中的"类型转换"或者叫"类型转型"问题。在许多程序语言中,不同的数据类型有不同的储存空间大小。例如,在C++中,short
类型通常是2字节大小,而long
类型(在这里被表示为1L)通常是4字节或8字节大小。
第一句代码中,你正在试图向一个short
类型的i
添加一个long
类型的数值1L,结果还尝试赋值给i
。因为long
类型的数据比short
更大,这可能会导致数据溢出,也就是说,有可能i
无法储存该数值。因此,编译器可能会抛出错误。
这段话中的"在数据安全的情况下大类型的数据向小类型的数据转换一定要显示的强制类型转换",就是在讲述这样一个原则,如果你一定要将大类型的数据赋值给小类型的变量,你需要显式地进行类型转换,并确保这样的操作是安全的,并不会引起数据溢出等问题。
给你一个简单的类型转换例子就是这样的:i = (short)(i + 1L)
,这样就将i + 1L
的计算结果强制转换为short
类型,然后再赋值给i
。这样的操作需要你了解并保证转换结果不会引起数据溢出。
126. && 和 & 、 || 和 | 有什么区别?
- & 和 | 对操作数进行求值运算,&& 和 || 只是判断逻辑关系
- && 和 || 在判断左侧操作数就能确定结果的情况下就不再对右侧操作数求值
请注意:
在编程的时候有些时候将 && 或 || 替换成 & 或 | 没有出错,但是其逻辑是错误的,可能会导致不可预想的后果。(比如当两个操作数一个是 1 一个是 2的时候)
127. C++的引用和C语言的指针有什么区别
指针和应用主要有以下区别:
- 引用必须被初始化,但是不分配空间。指针不声明时初始化,在初始化的时候需要分配存储空间。
- 引用初始化以后不能被改变,指针可以改变所指的对象。
- 不存在指向空值的引用,但是存在指向空值的指针。
请注意:
引用作为函数参数时,会引发一定的问题,因为让引用做参数,目的就是想改变这个应用所指向地址的内容,而函数调用时传入的是实参,看不出函数的参数是正常变量,还是引用,因此可能会引发错误,所以使用的时候一定要小心谨慎。
解释:
这段话是关于程序设计中函数参数选择的讨论。在许多编程语言中,函数的参数可以是普通的变量值(值传递)或者引用(引用传递)。
现在,让我们解读一下其中的含义:
-
“让引用做参数,目的就是想改变这个引用所指向地址的内容”: 这是引用传递的基本概念。当你将变量作为引用传递时,你并不是在复制一份新的变量,而是在操作原有的变量。所以,任何在函数中对该引用的修改,将影响到原有的变量。
-
“函数调用时传入的是实参,看不出函数的参数是正常变量,还是引用”: 这里的含义是说从调用者角度,无法直观看出函数是通过引用传递还是值传递,除非你熟悉函数的定义。也就是说,如果你调用一个使用引用参数的函数,而你没有注意到这一点,你可能会意外地改变了你传入的变量。
-
“因此可能会引发错误,所以使用的时候一定要小心谨慎”:由于以上的原因,不小心的使用引用传递可能会导致错误。所以,当你在编写一个接受引用参数的函数,或者在调用一个这样的函数时,你需要格外注意。
总的来说,让引用作为函数参数能够使我们更灵活的编写代码,但同时也需要我们格外小心,因为对引用的修改会影响到原始变量。
128. 写一个“标准”的宏MIN
问 : 写一个“标准”宏MIN , 这个宏输入两个参数并且返回较小的一个
答 :
#define min(a,b)((a) <= (b) ? (a) : (b))
129. typedef 和 define有什么区别?
- 用法不同:typedef用来定义一种数据类型的别名,增强程序的可读性。define主要是用来定义常量,以及书写复杂、使用频繁的宏。
- 执行时间不同:typedef是编译过程的一部分,有类型检查的功能。define是宏定义,是预编译的部分,其发生在编译之前,只是简单的进行字符串替换,不进行类型的检查。
- 作用域不同:typedef有作用域的限定,define不受作用域约束,只要在define声明后的应用都是正确的。
- 对指针的操作不同:typedef和define定义的指针有很大区别。
请注意:
typedef定义是语句,句尾要加上分号。而define不是语句,句尾不加分号。
解释:
这里前三点都很好理解,就着重讲解一下第四点.
typedef
用于定义类型的别名。它的语法形式是: typedef 原名 别名;
。
例如,定义一个int类型的指针别名pInt:
typedef int* pInt;
pInt a, b;
pInt a, b
中 a 和 b 都是int*
类型的,都是指向整型数据的指针,也就是说 pInt b
中的b也被认为是指针。
#define
是预处理器(预编译指令),它定义的是一个文本常量,不关心数据类型。在编译期对源代码进行一种简单的替换操作,其语法形式是: #define 旧名 新名
。
#define int* pInt
pInt a, b;
在定义 pInt a, b
时,a 是一个指向整型数据的指针,而b 成了一个整型变量。因为在预处理期,pInt a, b;
被替换成了 int* a, b;
,而在C++中,int* a, b
是声明一个指向int的指针 a 和一个int变量 b。
所以,如果你在使用指针别名,需要注意typedef
和#define
的区别,因为#define
只是简单的进行文本替换,而并不进行语法分析。所以对于某些情况下,typedef
和#define
是有区别的,尤其在定义指针类型时,typedef
会更为灵活和安全。
130. 关键字const是什么?
const用来定义一个只读的变量或对象。
主要优点:便于类型检查、同宏定义一样可以方便的进行参数的修改和调整、节省空间、避免不必要的内存分配、可为函数重载提供参考。
请注意:
const修饰函数参数,是一种编程规范的要求,便于阅读,一看就知道这个参数不能被改变,实现时不易出错。
解释:
-
const
定义只读变量或对象:const
关键字可以用来定义常量,这意味着一旦定义后,其值就不能再改变。例如,const int a = 10;
就定义了一个整型的常量 a,其值为10,是不能再被修改的。 -
便于类型检查:因为
const
常量有明确的类型(如int, float, Object等),编译器对其进行类型检查,这可以避免因类型错误而导致的严重错误。 -
与宏定义一样可以方便的进行参数的修改和调整:宏定义(#define)和const常量都可以在代码中定义一些固定值。这些固定值如果在多处使用,当需要修改时,只需修改定义处的值,而无需在代码的多个位置进行修改。
-
节省空间 避免不必要的内存分配:
const
常量在定义时需要明确初始值,因此,它们的存储结果在编译时就已经确定,不需要在运行时动态分配内存,这样可以节省程序运行时的开销。 -
可为函数重载提供参考:在C++中,函数的重载可以根据函数参数是否为const来判断。比如有两个函数
void foo(int& a);
和void foo(const int& a);
在调用foo(a);
时,如果a是const类型,会调用后者,否则调用前者。这样就可以确保当传入的参数为const时,函数不会改变其值。
131. static有什么作用?
static在C
中主要是用于定义全局静态变量、定义局部静态变量、定义静态函数。
在C++
中新增了两种作用:定义静态数据成员、静态成员函数。
请记住:
因为static定义的变量分配在静态区,所以其定义的变量的默认值为0,普通变量的默认值为随机数,在定义指针变量时要特别注意。
132. extern有什么用?
extern标识的变量或者函数声明其定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。
133. 流操作符重载为什么返回引用?
在程序中,流操作符 >> 和 << 经常连续使用。因此这两个操作符的返回值应该是一个仍旧支持这两个操作符的流引用。其他的数据类型都无法做到这一点。
请记住:
除了在赋值操作符和流操作符之外的其他的一些操作符中,如+,-,* ,/ 等却千万不能返回引用。因为这四种和操作符的对象都是右值,因此,他们必须构造一个对象作为返回值。
解释:
这里的描述是关于C++中流插入操作符 << 和流提取操作符 >> 重载的问题。
我们通常在做输入输出操作时,会连续使用 << 或者 >> 操作符,像这样:cout << "Hello " << “World”;
这里,"Hello " 和 “World” 都是发送到cout流中的,因此,cout << "Hello " 必须返回一个可以继续接受插入操作的对象。在这种情况下,最佳的选择就是返回cout对象本身,但是由于连续操作需要连贯性,我们不能返回一个新的副本,因为副本和原始的流对象并不是同一个对象。相反,我们返回了这个cout流对象的引用,这样我们就可以连续进行插入操作了。
这就是为什么在C++中,当我们重载流插入操作符和流提取操作符时,我们会让这些操作符返回一个ostream或istream的引用,保证连续操作的连贯性。
所以,这段话的意思就是,因为 >> 和 << 操作符经常进行连续操作,我们需要这两个操作符的返回值是一个可以进行连续操作的流引用。由于只有流这种数据类型支持这两个操作,所以必须要返回一个流引用。
134. 简述指针常量和常量指针之间的区别。
指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变。
常量指针是指定义了一个指针,这个指针指向一个只读的对象,不能通过常量指针来改变这个对象的值。
指针常量强调的是指针的不可改变性,而常量指针强调的是指针对其所指对象的不可改变性。
请注意:
无论是指针常量还是常量指针,其最大的用途就是作为函数的形式参数,保证实参在被调用函数中的不可改变特性。
解释:
这段话的核心是讨论C++中的"指针常量"和"常量指针"两种类型,并强调它们在函数参数中的应用。
-
“指针常量”:指针本身是常量,指向的数据可以改变。形式为:int* const ptr; 这意味着ptr的值(也即它所指向的地址)不能改变,但是该地址处存储的数据是可以改变的。
-
“常量指针”:指针指向的数据是常量,指针本身可以改变。形式为:const int* ptr 或 int const* ptr;这意味着ptr所指向的值不能改变,但是ptr自身(存储地址的值)可以改变。
这两种类型的指针在作为函数参数时,都有确保数据不被错误修改的作用。当我们不希望函数内部修改我们传递的参数值时,可以使用这两种类型的指针。
例如,如果我们有一个函数,它的参数是一个指针,并且我们不想这个函数修改我们传递给它的数据,我们就可以将该函数的参数设置为"常量指针"。这样,函数就不能通过这个指针来修改数据了。
或者,如果我们不想函数改变指针的指向,那我们就可以将该函数的参数设置为"指针常量"。
总的来说,这段话的意思就是,“指针常量”和“常量指针”在函数参数中的最主要用途就是保护我们传递给函数的数据或地址不被错误地修改。
135. 数组名和指针的区别
请写出以下代码的打印结果:
#include <iostream.h>
#include <string.h>
void main(void)
{
char str[13]="Hello world!";
char *pStr="Hello world!";
cout<<sizeof(str)<<endl;
cout<<sizeof(pStr)<<endl;
cout<<strlen(str)<<endl;
cout<<strlen(pStr)<<endl;
return;
}
正确答案是:13 , 8 , 12 , 12
请记住:
一定要记得数组名并不是真正意义上的指针,它的内涵要比指针丰富得多。但是当数组名当作参数传递给函数后,其失去原来的含义,变作普通的指针。
另外要记住,sizeof
不是函数,只是操作符。
解释:
-
sizeof(str)
:- 语句:
sizeof(str)
- 解释:
str
是一个数组,定义为char str[13]
。sizeof
操作符返回整个数组的大小(即数组的字节数)。这里str
包含了13个字符(包括空终止符)。 - 输出:
13
,因为数组str
占据13个字节。
- 语句:
-
sizeof(pStr)
:- 语句:
sizeof(pStr)
- 解释:
pStr
是一个指针,指向常量字符数组。sizeof
操作符返回指针本身的大小,而不是指针所指向的数据大小。指针的大小与系统和编译器有关,但在大多数现代平台上(如64位系统),指针大小通常是8字节,而在32位系统中通常是4字节。 - 输出:在64位系统上为
8
(假设是64位系统),因为指针大小为8字节(在32位系统上通常为4字节)。
- 语句:
-
strlen(str)
:- 语句:
strlen(str)
- 解释:
strlen
函数用于计算字符串的长度(不包括空终止符)。它需要#include <cstring>
来使用。 - 输出:
12
,因为"Hello world!"是一个12个字符的字符串,不包括\0
终止符。
- 语句:
-
strlen(pStr)
:- 语句:
strlen(pStr)
- 解释:与上面相同,
strlen
函数用于计算字符串的长度(不包括空终止符)。这里pStr
指向一个字符串常量,内容为"Hello world!"。 - 输出:
12
,因为"Hello world!"的长度同样是12个字符,不包括\0
终止符。
- 语句:
136. 如何避免野指针?
“野指针”的产生原因和解决办法如下:
- 指针变量声明时没有被初始化。解决办法:指针声明时初始化,可以是具体的地址值,也可以让它指向NULL。
- 指针p被free或者delete之后,没有置为NULL。解决办法:指针指向的内存空间被释放后指针应该指向NULL。
- 指针操作超越了变量的作用范围。解决办法:在变量的作用域结束前释放掉变量的地址空间,并且让指针指向NULL。
请注意:
“野指针”的解决方案也是编程规范的基本原则,平时使用指针时一定要避免“野指针”。在使用指针前一定要检验指针的合法性。
137. 常引用有什么作用?
常引用的引入主要是为了避免使用变量的引用时,在不知情的情况下改变变量的值。常引用主要用于定义一个普通变量的只读属性的别名、作为函数的传入形参、避免实参在调用函数中被意外的改变。
请注意:
在很多情况下,需要用常引用做形参,被引用对象等效于常对象,不能在函数中改变实参的值,这样的好处是有较高的易读性和较小的出错率。
解释:
这段话是在讲述C++中常引用(const reference)的使用背景和主要用途。
常引用是对变量的一个别名,但是这个“别名”是常量,它不允许通过它来改变所引用的变量的值。
在以下情况下,我们通常会使用常引用:
-
当我们希望有一种方式来访问变量,但是同时又不想改变它的值时,我们可以使用常引用。比如,我们想打印一个变量,但又不想改变它,我们就可以用常引用。
-
当我们希望传递一个大的数据结构给函数,但又不希望这个函数改变这个数据结构时,我们可以使用常引用作为函数的参数。因为引用可以避免数据的拷贝,而常修饰符能够确保数据不被改变,这样我们就既节省了时间又保证了安全。
总的来说,这段话是在讲述常引用维护了引用的优势(避免拷贝),同时扩展了它的功能(防止不必要的修改)。这就是常引用被引入的主要原因,也是它的主要用途。
示例:计算字符串的长度
假设我们有一个函数需要计算字符串的长度。我们可以通过按值传递或按引用传递来实现这个功能。让我们对比一下两种方法的不同之处。
1. 按值传递
#include <iostream>
#include <string>
int getLengthByValue(std::string str) {
return str.length();
}
int main() {
std::string myString = "Hello, world!";
std::cout << "Length: " << getLengthByValue(myString) << std::endl;
return 0;
}
在这个例子中,getLengthByValue
函数按值传递字符串 str
,这意味着每次调用该函数时,都会复制字符串对象。对于较大的字符串,这种复制操作会有一定的性能开销。
2. 按常引用传递
#include <iostream>
#include <string>
int getLengthByConstReference(const std::string& str) {
return str.length();
}
int main() {
std::string myString = "Hello, world!";
std::cout << "Length: " << getLengthByConstReference(myString) << std::endl;
return 0;
}
在这个例子中,getLengthByConstReference
函数按常引用传递字符串 str
。由于使用了 const
关键字,传入的字符串在函数内部无法被修改。同时,传递的是引用而不是副本,因此不会有复制操作,大大提高了函数的效率,尤其是当字符串较大时。
总结
通过使用常引用,我们避免了复制大的对象,同时确保了传入对象在函数内的不可变性,从而提高了程序的效率和安全性。这种方法特别适用于需要传递较大对象或不希望对象被修改的场景。
138. 简述strcpy 、 sprintf 与 memcpy的区别
三者主要有以下不同:
- 操作对象不同,strcpy的两个操作对象均为字符串,sprintf的操作源对象可以是多种数据类型,目的操作对象是字符串,memcpy的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。
- 执行效率不同,memcpy最高,strcpy次之,sprintf的效率最低
- 实现功能不同,strcpy主要实现字符串变量之间的拷贝,sprintf主要实现其他数据类型格式到字符串的转化,memcpy主要是内存块之间的拷贝。
请注意:strcpy、sprintf和memcpy都可以实现拷贝功能,根据实际需求,来选择合适的函数实现拷贝功能。