C/C++『概念理解』
1 变量的声明和定义有什么区别
- 变量的声明:告诉编译器变量的名称和类型称为声明,变量的声明不为变量分配地址和存储空间。
- 变量的定义:为变量分配地址和存储空间的称为定义。
注意:一个变量可以在多个地方声明,但是只在一个地方定义。很多时候一个变量,只是声明不分配内存空间,直到具体使用时才初始化,分配内存空间,如外部变量。
2 bool、int、float、指针变量与“零值”比较的if 语句
bool 类型:
1
2
3
4
5
6
7
8if( flag )
{
A;
}
else
{
B;
}int 类型:
1
2
3
4
5
6
7
8if( 0 != flag )
{
A;
}
else
{
B;
}float 类型:
1
2
3
4if ( ( flag >= NORM ) && ( flag <= NORM ) )
{
A;
}指针类型:
1
2
3
4
5
6
7
8if( NULL == flag )
{
A;
}
else
{
B;
}
3 sizeof 和 strlen 的区别
- sizeof是操作符,strlen是库函数
- sizeof的参数可以是数据类型,也可以是变量;strlen的参数必须是以”\0”结尾的字符串
- sizeof在编译器编译时出结果,而strlen在运行时才出结果
- sizeof计算的是数据类型占内存的大小,而strlen计算的是字符串的实际长度
4 C 语言的关键字 static 和 C++ 的关键字 static 有什么区别
- 在 C 中,static 用来是修饰局部静态变量和外部静态变量以及函数。
- 在 C++ 中,static 既可以用来是修饰局部静态变量和外部静态变量以及函数,也可以用来定义类的成员变量和函数。
5 C语言中的malloc和C++中的new的区别
- malloc、free 是函数,可以覆盖,在 C、C++ 中都可以使用。
- new、delete 是操作符,可以重载,只能在 C++中使用。
- malloc 仅仅分配内存,free 仅仅回收内存,并不执行构造和析构函数。
- new 可以调用对象的构造函数,对应的 delete 调用相应的析构函数。
- new、delete 返回的是某种数据类型指针,malloc、free 返回的是 void 指针。
6 “标准”宏 MIN和MAX
标准宏 MIN :
1
#define min(a,b)((a)<=(b)?(a):(b))
标准宏 MAX :
1
#define max(a,b)((a)>=(b)?(a):(b))
7 一个指针可以是 volatile 吗
可以,因为指针和普通变量一样,有时也有变化程序的不可控性。常见例:子中断服务子程序修改
一个指向一个 buffer 的指针时,必须用 volatile 来修饰这个指针。
注意:指针是一种普通的变量,从访问上没有什么不同于其他变量的特性。其保存的数值是个整型
数据,和整型变量不同的是,这个整型数据指向的是一段内存地址。
8 数组名 a 和 &a 的区别
数组名 a 是数组的首地址,而 &a 是数组的指针(即对数组名取地址的二级指针)
9 三种C/C++的内存分配机制
一个 C、C++ 程序编译时内存分为 5 大存储区:堆区、栈区、全局区、文字常量区、程序代码区。????
栈:存放函数的参数和局部变量,编译器自动分配和释放
堆:new关键字动态分配的内存,由程序员手动进行释放,否则程序结束后,由操作系统自动进行回收
自由存储区:由malloc分配的内存,和堆十分相似,由对应的free进行释放
全局区:包含静态区和全局区。用于存放静态变量和全局变量
常量区:存放常量,不允许被修改
9.1 从静态存储区域分配
内存在程序编译时就已经分配好,这块内存在程序的整个运行期间都存在。
优缺点:速度快、不容易出错,因为有系统会善后。例如全局变量,static 变量等。
9.2 从栈上分配
在执行函数时,函数内局部变量的存储单元都在栈上创建,函数执行结束时这些存储单元自动被释
放。
优缺点:栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
9.3 从堆上分配(动态内存分配)
程序在运行的时候用 malloc 或 new 申请任意大小的内存,程序员自己负责在何时用 free 或 delete 释放内存。
优缺点:动态内存的生存期由程序员决定,使用非常灵活。如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,另外频繁地分配和释放不同大小的堆空间将会产生堆内碎块。
10 strcpy、sprintf、memcpy 的区别
- 操作对象不同:strcpy 的两个操作对象均为字符串,sprintf 的操作源对象可以是多种数据类型,目的操作对象是字符串,memcpy 的两个操作对象为两个任意可操作的内存地址,并不限于何种数据类型。
- 执行效率不同:memcpy 最高,strcpy 其次,sprintf 的效率最低。
- 实现功能不同:strcpy 主要实现字符串变量间的拷贝,sprintf 主要实现其他数据类型格式到字符串的转化,memcpy 主要是内存块间的拷贝。
11 设置地址为 0x67a9 的整形变量值为 0xaa66
1 |
|
注意:一个整型数据可以强制转换成地址指针类型,只要有意义即可
12 面向对象的三大特征
面向对象的三大特性包括封装性、继承性和多态性。
12.1 封装性
将客观事物抽象成类,每个类对应自己的数据和方法实行 protection( private、protected、public )
12.2 继承性
广义继承有三种实现形式:实现继承、可视继承、接口继承
- 实现继承:使用基类的属性和方法而无需额外编码的能力
- 可视继承:子窗体使用父窗体的外观和实现代码
- 接口继承:仅使用属性和方法,实现滞后到子类实现
12.3 多态性
是将父类对象设置成为和一个或更多它的子对象相等的技术。用子类对象给父类对象赋值之后,父类对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。
13 C++ 空类默认产生的6个类成员函数
- 缺省构造函数
- 缺省拷贝构造函数
- 缺省析构函数
- 缺省赋值运算符:赋值运算符讲解
- 缺省取址运算符(缺省的取地址运算符)
- 缺省取址运算符 const
1 |
|
注意:有些书上只是简单的介绍了前四个函数。没有提及后面这两个函数。但后面这两个函数也是空类的默认函数。另外需要注意的是,只有当实际使用这些函数的时候,编译器才会去定义它们。
14 拷贝构造函数和赋值运算符重载的不同
- 拷贝构造函数生成新的类对象,而赋值运算符不能
- 由于拷贝构造函数是直接构造一个新的类对象,所以在初始化这个对象之前不用检验源对象是否和新建对象相同。而赋值运算符则需要这个操作,另外赋值运算中如果原来的对象中有内存分配要先把内存释放
注意:当有类中有指针类型的成员变量时,一定要重写拷贝构造函数和赋值运算符,不要使用默认的。
15 类成员函数的重写、重载和隐藏的区别
15.1 重写和重载的区别
- 范围区别:被重写的和重写的函数在两个类中,而重载和被重载的函数在同一个类中。
- 参数区别:被重写函数和重写函数的参数列表一定相同,而被重载函数和重载函数的参数列表一
定不同。 - virtual区别:重写的基类中被重写的函数必须要有 virtual 修饰,而重载函数和被重载函数可以被virtual 修饰,也可以没有。
15.2 隐藏和重写、重载的区别
- 与重载的范围不同:和重写一样,隐藏函数和被隐藏函数不在同一个类中。
- 参数的区别:隐藏函数和被隐藏的函数的参数列表可以相同,也可不同,但是函数名肯定要相同。
当参数不相同时,无论基类中的参数是否被 virtual 修饰,基类的函数都是被隐藏,而不是被重写。
注意:虽然重载和覆盖都是实现多态的基础,但是两者实现的技术完全不相同,达到的目的也是完
全不同的,覆盖是动态态绑定的多态,而重载是静态绑定的多态。
16 多态的实现原理
编译器发现一个类中有虚函数,便会立即为此类生成虚函数表 vtable。虚函数表的各表项为指向对应虚函数的指针。编译器还会在此类中隐含插入一个指针 vptr(对 vc 编译器来说,它插在类的第一个位置上)指向虚函数表。调用此类的构造函数时,在类的构造函数中,编译器会隐含执行 vptr 与 vtable 的关联代码,将 vptr 指向对应的 vtable,将类与此类的 vtable 联系了起来。另外在调用类的构造函数时,指向基础类的指针此时已经变成指向具体的类的 this 指针,这样依靠此 this 指针即可得到正确的 vtable,如此才能真正与函数体进行连接,这就是动态联编,实现多态的基本原理。
注意:一定要区分虚函数,纯虚函数、虚拟继承的关系和区别。牢记虚函数实现原理,因为多态C++面试的重要考点之一,而虚函数是实现多态的基础。
17 链表和数组的区别
- 存储形式:数组是一块连续的空间,声明时就要确定长度。链表是一块可不连续的动态空间,长度可变,每个结点要保存相邻结点指针。
- 数据查找:数组的线性查找速度快,查找操作直接使用偏移地址。链表需要按顺序检索结点,效率低。
- 数据插入或删除:链表可以快速插入和删除结点,而数组则可能需要大量数据移动。
- 越界问题:链表不存在越界问题,数组有越界问题。
注意:在选择数组或链表数据结构时,一定要根据实际需要进行选择。数组便于查询,链表便于插入删除。数组节省空间但是长度固定,链表虽然变长但是占了更多的存储空间。
18 队列和栈的异同
队列和栈都是线性存储结构,但是两者的插入和删除数据的操作不同,队列是“先进先出”,栈是“后进先出”。
注意:区别栈区和堆区。堆区的存取是“顺序随意”,而栈区是“后进先出”。栈由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。堆一般由程序员分配释放, 若程序员不释放,程序结束时可能由 OS 回收。分配方式类似于链表。堆栈只是一种数据结构,而堆区和栈区是程序的不同内存存储区域。
19 对于规范编程的理解或认识
编程规范可以总结为:程序的可行性、可读性、可移植性以及可测试性
注意:这是编程规范的总纲目,面试者不一定要去背诵上面给出的那几个例子,应该去理解这几个
例子说明的问题,想一想,自己如何解决可行性、可读性、可移植性以及可测试性这几个问题,结合以
上几个例子和自己平时的编程习惯来回答这个问题。
20 short i = 0; i = i + 1L;这两句有错吗
代码一是错的,代码二是正确的
注意:在数据安全的情况下大类型的数据向小类型的数据转换一定要显示的强制类型转换。
21 &&和&、||和|有什么区别
- &和|对操作数进行求值运算,&&和||只是判断逻辑关系。
- &&和||在在判断左侧操作数就能确定结果的情况下就不再对右侧操作数求值。
注意:在编程的时候有些时候将&&或||替换成&或|没有出错,但是其逻辑是错误的,可能会导致不可预想的后果(比如当两个操作数一个是 1 另一个是 2 时)。
22 C++的引用和 C 语言的指针有什么区别
指针和引用主要有以下区别:
- 引用必须被初始化,但是不分配存储空间。指针不声明时初始化,在初始化的时候需要分配存储空间。
- 引用初始化以后不能被改变,指针可以改变所指的对象。
- 不存在指向空值的引用,但是存在指向空值的指针。
注意:引用作为函数参数时,会引发一定的问题,因为让引用作参数,目的就是想改变这个引用所指向地址的内容,而函数调用时传入的是实参,看不出函数的参数是正常变量,还是引用,因此可能会引发错误。所以使用时一定要小心谨慎。
23 typedef 和 define 的区别
- 用法不同:typedef 用来定义一种数据类型的别名,增强程序的可读性。define 主要用来定义
常量,以及书写复杂使用频繁的宏。 - 执行时间不同:typedef 是编译过程的一部分,有类型检查的功能。define 是宏定义,是预编译的部分,其发生在编译之前,只是简单的进行字符串的替换,不进行类型的检查。
- 作用域不同:typedef 有作用域限定。define 不受作用域约束,只要是在 define 声明后的引用都是正确的。
- 对指针的操作不同:typedef 和 define 定义的指针时有很大的区别。
注意:typedef 定义是语句,因为句尾要加上分号。而 define 不是语句,千万不能在句尾加分号。
24 关键字 const 是什么
const 用来定义一个只读的变量或对象。
主要优点:便于类型检查、同宏定义一样可以方便地进行参数的修改和调整、节省空间,避免不必要的内存分配、可为函数重载提供参考。
注意:const 修饰函数参数,是一种编程规范的要求,便于阅读,一看即知这个参数不能被改变,实现时不易出错。
25 static 有什么作用
static 在 C 中主要用于定义全局静态变量、定义局部静态变量、定义静态函数。在 C++中新增了两种作用:定义静态数据成员、静态函数成员。
注意:因为 static 定义的变量分配在静态区,所以其定义的变量的默认值为 0,普通变量的默认值为随机数,在定义指针变量时要特别注意。
26 extern 有什么作用
extern 标识的变量或者函数声明其定义在别的文件中,提示编译器遇到此变量和函数时在其它模块中寻找其定义。
27 流操作符重载为什么返回引用
在程序中,流操作符>>和<<经常连续使用。因此这两个操作符的返回值应该是一个仍旧支持这两个操作符的流引用。其他的数据类型都无法做到这一点。
注意:除了在赋值操作符和流操作符之外的其他的一些操作符中,如+、-、*、/等却千万不能返回引用。因为这四个操作符的对象都是右值,因此,它们必须构造一个对象作为返回值。
28 指针常量与常量指针的区别
- 指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变;常量指针是指定义了一个指针,这个指针指向一个只读的对象,不能通过常量指针来改变这个对象的值。
- 指针常量强调的是指针的不可改变性;而常量指针强调的是指针对其所指对象的不可改变性。
注意:无论是指针常量还是常量指针,其最大的用途就是作为函数的形式参数,保证实参在被调用函数中的不可改变特性。
29 数组名和指针的区别
举例如下:
1
2
3
4
5
6
7
8
9
10
11
12#include <iostream.h>
#include <string.h>
int 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 0;
}
输出结果:
1
2
3
413
4
12
12
注意:一定要记得数组名并不是真正意义上的指针,它的内涵要比指针丰富的多。但是当数组名当做参数传递给函数后,其失去原来的含义,变作普通的指针。另外要注意 sizeof 不是函数,只是操作符。
30 如何避免“野指针”
定义:野指针不是NULL指针,是未初始化或者未清零的指针,它可能指向一块程序员未知的受限内存地址。
产生野指针的原因及解决办法:
- 问题指针变量声明时没有被初始化。解决办法:指针声明时初始化,可以是具体的地址值,也可让它指向 NULL。
- 指针 p 被 free 或者 delete 之后,没有置为 NULL。解决办法:指针指向的内存空间被释放后指针应该指向 NULL。
- 指针操作超越了变量的作用范围,如比如数组b[10]范围为10,但指针为b+11。解决办法:在变量的作用域结束前释放掉变量的地址空间并且让指针指向 NULL。
注意:“野指针”的解决方法也是编程规范的基本原则,平时使用指针时一定要避免产生“野指针”,在使用指针前一定要检验指针的合法性。
31 常引用有什么作用
常引用的引入主要是为了避免使用变量的引用时,在不知情的情况下改变变量的值。常引用主要用于定义一个普通变量的只读属性的别名、作为函数的传入形参,避免实参在调用函数中被意外的改变。
注意:很多情况下,需要用常引用做形参,被引用对象等效于常对象,不能在函数中改变实参的值,这样的好处是有较高的易读性和较小的出错率。
32 用 C 编写一个死循环程序
代码如下:
1
while(1){ }
33 构造函数与析构函数能否为虚函数
构造函数不能是虚函数。而且不能在构造函数中调用虚函数,因为那样实际执行的是父类的对应函数,因为自己还没有构造好。
析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。析构函数也可以是纯虚函数,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。
注意:虚函数的动态绑定特性是实现重载的关键技术,动态绑定根据实际的调用情况查询相应类的
虚函数表,调用相应的虚函数。
33.1 构造函数不能为虚函数
- 从存储空间理解:虚函数对应一个 vtable ,而这个 vtable 其实是存储在对象的内存空间的。那么就有问题是:如果构造函数是虚的,就需要通过 vtable 调用,而此时对象还没有实例化,无法找到 vtable ,因此构造函数不能为虚函数 。
- 从使用角度理解:虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例的,那么使用虚函数也就没有实际意义,因此构造函数不能为虚函数。**(** 虚函数的作用在于通过父类的指针挥着引用来调用他的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的不可能通过父类指针或者引用去调用,因此构造函数不能为虚函数。**)**
- 从实现上理解:vbtl 在构造函数调用后才建立,因此构造函数不能为虚函数。
- 综上所述,构造函数不需要是虚函数,也不允许是虚函数,因为创建一个对象时我们总是要明确指定对象的类型,尽管我们肯能通过基类的指针或者引用去访问它。但是析构却不一定,我们往往通过及尅的指针来销毁对象。这时候如果析构函数不是虚函数,就不能正确识别对象类型而不能正确调用析构函数。
33.2 析构函数最好是虚函数
当析构一个指向派生类的基类指针时,最好将基类的析构函数声明为虚函数,否则可以存在内存泄露的问题。
如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除指向派生类的基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。
34 对于面向对象的认识
面向对象可以理解成对待每一个问题,都是首先要确定这个问题由几个部分组成,而每一个部分其实就是一个对象。然后再分别设计这些对象,最后得到整个程序。传统的程序设计多是基于功能的思想来进行考虑和设计的,而面向对象的程序设计则是基于对象的角度来考虑问题。这样做能够使得程序更加的简洁清晰。
注意:编程中接触最多的“面向对象编程技术”仅仅是面向对象技术中的一个组成部分。发挥面向对象技术的优势是一个综合的技术问题,不仅需要面向对象的分析,设计和编程技术,而且需要借助必要的建模和开发工具。
35 静态绑定和动态绑定的介绍
静态绑定和动态绑定是C++多态的一种特性
35.1 对象的静态类型和动态类型
静态类型:对象在声明时采用的类型,在编译时确定
动态类型:当前对象所指的类型,在运行期决定,对象的动态类型可变,静态类型无法更改
35.2 静态绑定和动态绑定
静态绑定:绑定的是对象的静态类型,函数依赖于对象的静态类型,在编译期确定
动态绑定:绑定的是对象的动态类型,函数依赖于对象的动态类型,在运行期确定
注意:只有虚函数才使用的是动态绑定,其他的全部是静态绑定
35.3 为什么引用可以实现动态绑定
因为引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指的对象的实际类型所定义的。