1 static(静态)变量有什么作用
3个体明显的作用:1)在函数体内,静态变量具有“记忆”功能,即一个被声明为静态变量在一个函数被调用的过程中其值维持不变2)在模块内,它的作用域范围是有限制的,即如果一个变量被声明为静态的,那么该变量可以被模块内所有函数访问,但不能被模块外其他函数访问。3)内部函数应该在当前源文件中说明和定义,对于可在当前源文件以外使用的函数,应该在一个头文件中说明,使用这些函数的源文件要包含这个头文件。
与局部变量和全局变量的区别:static全局变量和普通全局变量相比的区别在于static全局变量只初始化一次,这样做的目的是为了防止在其他文件单元中被引用。static局部变量和普通局部变量的区别是static局部变量只被初始化一次,下一次的运算依据是上一次的结果值。static函数与普通函数的区别在于作用域不一样,static函数只在一个源文件中有效,不能被其他源文件使用。
为什么static变量值初始化一次:
对于所有对象(不仅仅是静态对象),初始化都只有一次,而由于静态变量具有“记忆”功能,初始化后,一直都没有被销毁,都会保存在内存区域中,所以不会再次初始化。(记住:初始化语句只执行一次)。
例如:
#include<stdio.h>
void fun(int i)
{
static int value=i++;
printf("%d\n",value);
}
int main()
{
fun(0);
fun(1);
fun(2);
return 0;
}
程序输出:
0
0
0
程序每次都输出0,是因为value是静态类型,只会定义一次。也就是说,不管调用fun()这个函数多少次,static int value=i++这个定义语句只会在第一次调用的时候执行,由于第一次执行的时候i=0,所以value也就被初始化为0了,以后调用fun()都不会再执行这条语句的。
在头文件中定义静态变量,是否可行?为什么?
不可行,如果在头文件中定义静态变量,会造成资源浪费的问题,同时也可能引起程序错误。因为如果在使用了这个头文件的每个C 语言文件中定义静态变量,按照编译的步骤,在每个头文件中都存在一个静态变量,从而会引起空间浪费和程序错误。所以不推荐在头文件中定义任何变量,当然也包括静态变量。
2 const有哪些作用:
1)定义const常量,具有不可变性。
2)进行类型检查,是编译器对处理内容有更多的了解,消除了一些隐患。
3)避免意义模糊的数字出现,同样可以很方便地进行参数的调整和修改。
4)保护被修饰的东西,防止被意外的修改,增强了程序的健壮性。
5)为函数重载提供参考
6)节省空间,避免不必要的内存分配
7)提高了程序的效率。编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译器间的常量,没有了存储与读内存的操作,使得它的效率也很高。
为什么要使用const引用?
一般引用初始化一个左值的时候,没有任何问题;而当初始化值不是一个左值时,则只能对一个常引用赋值,而且这个赋值是有一个过程的,首先将值隐式转换到类型T,然后将这个转换结果存放在一个临时对象里,最后用这个临时对象来初始化这个引用变量。
const引用可以初始化为不同类型的对象或者初始化为右值,如字面值常量,而非const引用只能绑定到该引用同类型的对象。
3 volatile在程序设计中有什么作用?
volatile 是一个修饰符,它用来修饰被不同线程访问和修改的变量。被volatile类型定义的变量,系统每次用到它的时候都是直接从对应的内存当中提取,而不会利用cache中的原有数值,以适应它的未知何时会发生的变化,系统对这种变量的处理不会做优化。所以,volatile一般用于修饰多线程间被多个任务共享的变量和并行设备硬件寄存器等。
4 char str[]="abc";char str2[]="abc";str1与str2不相等,为什么?
两者不相等,是因为str1和str2都是字符数组,每个都有其自己的存储区,它们的值则是各存储区的首地址。
但是,对于const char *str3="abc"和const char *str4="abc";则不一样,str3和str4是字符指针而非字符数组,并不分配内存,其后的“abc”存放在常量区,str3和str4是指向它们指向的地址的首地址,而它们自己仅是指向该区首地址的指针,所以相等。
5 C++里面是不是所有的动作都是main()函数引起的,但是一个C语言程序总是从main()函数开始执行的。
不是,对于C++程序而言,静态变量、全局变量、全局对象的分配早在main()函数之前已经完成。所以并不是所有的动作都是main()引起的,只是编译器是由main()开始执行的,main()只不过是一个约定的函数入口,在main()函数中的显示代码之前,会调用一个由编译器生成的_main()函数,而_main()函数会进行所有全局对象的构造及初始化工作。
在main()函数退出后再执行一段代码?答案依然是全局对象,当程序退出后,全局变量必须销毁,自然会调用全局对象的析构函数,所以剩下的就同构造函数一样了。
6 前置运算和后置元素有什么区别?
以++操作为例,对于变量a,++a表示取a的地址,增加它的内容,然后把值放在寄存器中;a++表示取a的地址,把它的值放入寄存器中,然后增加内存中a的值。前置(++)通常要比后置自增(—++)效率更高。
例题:a是变量,执行(a++)+=a语句是否合法?
首先我们要清楚两个概念:左值和右值。左值就是可以出现在表达式左边的值(等号左边),可以被改变,它是存储数据值的那块内存的地址,也称为变量的地址;右值是指存储在某内存地址中的数据,也称为变量的数据。左值可以作为右值,但是右值不可以是左值。
本题不合法,a++不能当做左值使用。++a可以当作左值使用。++a表示取a的地址,对它的内容进行加1操作,然后把值放在寄存器中。a++表示取a的地址,把它的值装入寄存器,然后对内存中a的值执行加1操作。
7 new/delete与malloc/free的区别是什么?
malloc/free是C/C++语言的标准库函数,在C语言中需要头文件<stdlib.h>的支持,new/delete是C++的运算符。对于类的对象而言,malloc/free无法满足动态对象的要求,对象在创建的同时要自动执行构造函数,对象消亡的之前要自动执行析构函数,而malloc/free不在编译器控制权限之内,无法执行构造函数和析构函数。
具体而已,new/delete和malloc/free的区别:
1)new能够自动计算需要分配的内存空间,而malloc需要手工计算字节数。
2)new和delete直接带具体类型的指针,malloc和free返回void的指针。
3)new是类型安全的,而malloc不是。
4)new一般由两步构成,分别是new操作和构造。new操作对应于malloc,但new操作可以重载,可以自定义内存分配策略,不做内存分配,甚至分配到非内存设备上,而malloc不行。
5)new将调用构造函数,而malloc不能;delete将调用析构函数,而free不能。
6)malloc/free需要库函数stdlib.h的支持,而new/delete不需要
需要注意的是,有资源的申请,就有资源的释放,否则就会出现资源泄漏的问题,所以new/delete,malloc/free必须配对使用。而delete和free被调用后,内存不会立即收回,指针也不会指向空,delete或free仅仅是高诉操作系统,这一块内存被释放了,可以用做其他用途。但是,由于没有重新对这块内存进行写操作,所以内存中的变量数值并没有发生变化,出现野指针的情况。因此,释放完内存后,应该将指针指向置位空。
8 已知String类定义,如何实现其函数体。
String类定义如下:
class String
{
public:
String(const char* str=NULL);
String(const String &another);
~String();
String &operator=(const String &rhs);
private:
char* m_data;
};
String::String(const char *str)
{
if(str==NULL)
{
m_data=new char[1];
m_data[0]='\0';
}
else
{
m_data=new char[strlen(str)+1];
strcpy(m_data,str);
}
}
String::String(const String &another)
{
m_data=new char[strlen(another.m_data)+1];
strcpy(m_data,another.m_data);
}
String::~String()
{
delete[] m_data;
}
String& String::operator=(const String &rhs)
{
if(this==&rhs)
return *this;
delete[] m_data;
m_data=new char[strlen(rhs.m_data)+1];
strcpy(m_data,rhs.m_data);
return *this;
}
9 栈空间的最大值是多少?
在Windows,栈是向低地址扩展的数据结构,是一块连续的内存的区域。栈顶的地址和栈的最大容量是系统预先规定好的,在Windows下,栈的大小是2MB。而申请堆空间的大小一般小于2GB.
由于内存的读取速度比硬盘快,当程序遇到大规模数据的频繁存取时,开辟内存空间很有作用。栈的速度快,但是空间小,不灵活。堆是向高地址扩展的,是不连续的内存区域。这是由于系统是用链表来存储空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址的,而堆的大小受限于计算机系统中的有效虚拟内存,所以堆获得的空间比较灵活,也比较大,但是速度相对慢一些。
10 指针和引用的区别
程序设计中的引用其实就是别名的意思,它用于定义一个变量来共享另一个变量的内存空间,变量是一个内存空间的名字,如果给内存空间起另一个名字,那就能够共享这个内存了,进而提高程序的开发效率。指针执行另一个内存空间的变量,可以通过它索引另一个内存空间的内容,而指针本身也有自己的内存空间。
引用与指针有着相同的地方,即指针指向一块内存,它的内容是所指内存的地址,引用是某块内存的别名。但是,两者并非完全相同,它们之间也存在着差别,具体表现在以下几个方面:
1)从本质上讲,指针是存放变量地址的一个变量,在逻辑上是独立的,它可以被改变,即其所指向的地址可以被改变,其指向的地址中所存放的数据也可以被改变。而引用则只是一个别名而已,它在逻辑上不是独立的,它的存在具有依赖性,所以引用必须在一开始就被初始化,而且其引用的对象在其整个生命周期中是不能被改变的,即自始自终只能依赖于同一个变量,具有“从一而终”的特性。
****2)作为参数传递时,两者不同。在C++语言中,指针与引用都可以用于函数的参数传递,但是指针传递参数和引用传递参数有着本质的不同。
指针传递参数本质上是值传递的方式,它所传递的是一个地址值(所有对形参的改变都只是这个地址值中存放变量的改变,而存放这个地址值的指针是不会变化的。如果要改变存放该地址值的指针,需要传入的是该指针的地址,所以可以使用指针的指针或者指针的引用。)。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,即在栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值。
而在引用传递过程中,被调用函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被船里成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调用函数对形参的任何操作都影响了主调函数中的实参变量。虽然它们都是在被调用函数栈空间上的一个局部变量,但是任何对引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针传递的参数,如果改变被调函数中的指针地址,它将影响不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量,那就得使用指向指针的指针,或者指针引用。
3)引用使用时不需要解引用(*),而指针需要解引用。
4)引用只能在定义时被初始化一次,之后不能被改变,即引用具有“从一而终”的特性,而指针却是可以改变的。
5)引用不可以为空,而指针可以为空。引用必须与存储单元相对应,一个引用对应一个存储单元。
6)对引用进行sizeof操作得到的是所指向的变量(对象)的大小,而对指针进行sizeof操作得到的是指针本身(所指向的变量或对象的地址)的大小。
7)指针和引用的自增(++)运算意义不一样。
8)如果返回动态分配的对象或内存,必须使用指针,引用可能引起内存泄漏。
11 指针和数组是否表示同一概念
主要表现在以下两方面的不同:
1)修改内容不同。
例如,char a[]="hello",可以通过去下标的方式对其进行修改,而对于char *p="word",此时p指向常量字符串,所以p[0]='x'是不允许的。
2)所占字节数不同
例如,char *p="world",p为指针,则sizeof(p)得到的是一个指针变量的字节数,而不是p所指的内存容量。
char a[]="hello world";
char *p=a;
在32位机器上,sizeof(a)=12字节,而sizeof(p)=4字节。
但要注意的是,当数组作为函数参数进行传递时,该数组自动退化为同类型的指针。
13 野指针?空指针?
野指针是指指向不可用内存的指针。任何指针变量在被创建时,不会自动成为NULL指针(空指针),其默认值是随机的,所以指针变量在创建的同时应当被初始化,或者将指针设置为NULL,或者让它指向合法的内存,而不应该放之不理,否则就会称为野指针。而同时由于指针被释放(free或delete)后,未能将其设置为NULL,也会导致该指针变为野指针。虽然free和delete把指针所指的内存给释放掉了,但它们并没有把指针本身释放掉,一般可以采用语句if(p!=NULL)进行防错处理,但是if语句却起不到作用,因为即使p不是NULL指针,它也不指向合法的内存块。第三种造成野指针的原因是指针操作超越了变量的作用范围。
14 #include<filename.h>和#include"filename.h"有什么区别
对于#include<filename.h>,编译器先从标准库路径开始搜索filename.h,然后从本地目录搜索,使得系统文件调用较快。而对于#include"filename.h",编译器先从用户的工作路径开始搜索filename.h,后去寻找系统路径,使得自定义文件较快。
15 宏的总结
1)宏与函数的区别
a 函数调用时,首先求出实参表达式的值,然后带入形参。而使用带参数的宏只是进行简单的字符替换
b 函数调用在程序运行时处理的,它需要分配临时的内存单元;而宏展开则是在编译时进行的,在展开时并不分配内存单元,也不进行值的传递处理,也没有“返回值”的概念。
c 对函数中的实参和形参都有定义类型,两者的类型要求一致。而宏不存在类型问题,宏名无类型,它的参数也无类型,只是一个符号代表,展开时带入指定的字符即可。
d 调用函数只可能得到一个返回值,而用宏可以设法得到几个结果。
e 使用宏次数多时,宏展开后源程序会变很长,因为每展开一次都是程序内容增长,而函数调用不会使源程序变长。
f 宏替换不占用运行时间,而函数调用则占用运行时间
g 参数每次用于宏定义时,它们都要重新求值,由于多次求值,具有副作用的参数可能会产生不可预料的结果。
2)枚举和define有什么不同
a 枚举常量是实体中的一种,而宏定义不是实体
b 枚举常量属于常量,而宏定义不是常量
c 枚举常量具有类型,但宏没有类型,枚举变量具有与普遍变量相同的性质,如作用域、值等,但宏没有。
d #define宏常量是在预编译阶段进行简单替换,枚举常量则是在编译的时候确定其值。
e 一般在编译器里,可以调试枚举常量,但是不能调试宏常量
f 枚举可以一次定义大量相关的常量,而#define宏一次只能定义一个
3)typedef和define的区别
a原理不同。#define是C语言中定义的语法,它是预处理指令,在预处理时进行简单的字符替换,不作正确性检查。typedef是关键字,它在编译时处理,所以typedef有类型检查的功能
b 功能不能。typedef用来定义类型的别名,这些类型可以是内置类型也可以是用户自定义的类型。#define不只是可以为类型去名字,还可以定义常量、变量、编译开关
c 作用域不同。#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而typedef有自己的作用域
d 对指针的操作不同。两者修饰指针类型时,作用不同。
4)宏定义与inline函数的区别
a 宏定义是在预处理阶段进行代码替换,而内联函数是在编译阶段插入代码;
b 宏定义没有类型检查,而内联函数有类型检查。
5)define和const的区别
a define只是用来进行单纯的文本替换,不分配内存空间,而const常量存在于程序的数据段,并在堆栈中分配了空间
b const常量有数据类型,而define常量没有数据类型
c 很多IDE支持调试const定义的常量,而不支持define定义的常量
16 C语言中struct和union的区别是什么
a 结构体与联合体虽然都是由多个不同的数据类型组成的,但不同之处在于联合体中所有成员共用同一地址空间,即联合体只存了一个被选择的成员,而结构体中所有成员占用空间是累加的,其所有成员都存在的,不同成员会存在不同的地址。
b 对于联合体的不同成员赋值,将会对其他成员重写,原来成员的值就不存在了,而对结构体的不同成员赋值是互不影响的
17 C语言和C++中struct的区别
a C语言的struct不能有函数成员,而C++的struct可以有
b C语言的struct中数据成员没有private、public和protected访问权限的设定,而C++的struct成员有访问权限限定。
c C语言的struct是没有继承关系的,而C++的struct有丰富的继承关系
18 C++中struct和class的区别
a class默认是private的,而struct默认是public的
b class可以用于定义模板,就像typename,而struct不可以
17 位运算总结
1)如何快速求取一个整数的7倍?
(X<<3)-X;
2)如何实现位操作求两个数的平均值
一般而言,求平均值可以使用(x+y)>>1,但是x+y可能移除,所以不使用加入,而使用异或和与运算实现加法:
(x&y)+(x^y)>>1
3)如何利用位运算计算数的绝对值?
以x为负数为例来分析,因为在计算机中,数字都是以补码的形式存在的,求负数的绝对值,应该是不管符号位,执行按位求反,末尾加1操作即可。
对于一个负数,将其右移31位后会变成0xffffffff,而对于一个正数而言,右移31位则为0x00000000,而0xffffffff^x+x=-1,因为任何数与1111异或,其实质都是把x的0和1进行颠倒计算。如果用变量y表示x右移31为,则(x^y)-y则表示的是x的绝对值。
18 考虑n为二进制,有多少个数中不存在两个相邻的1.
当n=1时,满足条件的二进制数为0、1,一共两个数;当n=2时,满足条件的二进制数有00、01、10,一共3个数;当n=3时,满足条件的二进制数有000、001、010、100、101,一共5个数。对n位二进制数,设所求结果a(n),对于第n位的值,分为0或者1两种情况:
1)第n位为0,则有a(n-1)个数。
2)第n位为1,则要满足没有相邻万为1的条件,第n-1位为0,有a(n-2)个数,因此得出结论a(n)=a(n-1)+a(n-2)
满足斐波拉契数列。
19 函数指针和指针函数的区别
指针函数是指带指针的函数,本质上是一个函数,函数返回类型是某一类型的指针。其形式一般如下所示:
类型标识符 *函数名(参数列表)
例如,int * f(x,y),它的意思是声明一个函数f(x,y),该函数返回类型为int型指针。
而函数指针是指向函数的指针,即本质是一个指针变量,表示的是一个指针,它指向的是一个函数。其形式一般如下所示:
类型说明符 (*函数名)(参数)
例如,int (*pf)(int x)它的意思就是声明一个函数指针,而pf=func则是将func函数的首地址赋值给指针。
引申:
1)数组指针/指针数组
数组指针就是指向数组的指针,它表示的是一个指针,它指向的是一个数组,它的重点是指针。例如,int(*pa)[8]声明了一个指针,该指针指向了一个有8个int型元素的数组。数组指针类似于二维i数组。即int a[][8];
指针数组就是指针的数组,表示的是一个数组,它包含的元素是指针,它的重点是数组。例如,int *ap[8]声明了一个数组,该数组的每一个元素都是int型的指针。
2)函数模板/模板函数
函数模板是对一批模样相同的函数的说明描述,它不是某一具体的函数;而模板函数则是将函数模板内的“数据类型参数”具体化得到的重载函数(就是由模板而来的函数简单地说,函数模板是抽象的,而模板函数则是具体的。
函数模板减少了程序员输入代码的工作量,是C++中功能最强的特性之一,是提高软件代码重用率的重要手段之一。函数模板的形式一般如下所示:
template<模板类型形参表>
<返回值类型> <函数名>(模板函数形参表)
{
//函数体
}
其中<模板函数形参表>的类型可以是任何类型。需要注意的是,函数模板并不是一个实实在在的函数,它是一组函数的描述,它并不能直接执行,需要实例化成模板函数后才能执行,而一旦数据类型形参实例化以后,就会产生一个实实在在的模板函数了。
3)类模板/模板类
类模板与函数模板类似,将数据类型定义为参数,描述了代码类似的部分类的集合,具体化为模板类后,可以用于生存具体的对象。
template<类型参数表>
class<类名>
{
//类说明体
};
template<类型形参表>
<返回类型><类名><类型名表>::<成员函数1>(形参表)
{
//成员函数定义体
}
其中<类型形参表>与函数模板中的一样,而类模板本身不是一个真实的类,只是对类的一种描述,必须用类型参数将其实例化为模板类后,才能用来生成具体的对象。简而言之,类是对象的抽象,而类模板就是类的抽象。
C++中引入模板类主要有以下5个方面的好处:
1)可用来创建动态增长和减少的数据结构
2)它是类型无关的,因此具有很高的可复用性
3)它在编译时而不是运行时检查数据类型,保证了类型安全
4)它是平台无关的,可移植性强
5)可用于基本数据类型
4)指针常量/常量指针
指针常量是指定义的指针只能在定义的时候初始化,之后不能改变其值。其格式为:
[数据类型][*][const][指针常量名称]
例如:char *const p1; int *const p2;(顶层const)
常量指针的值不能改变,但是其指向的内容却可以改变。
常量指针是指指向常量的指针,因为常量指针指向的对象是常量,因此这个对象的值是不能够改变的。定义的格式如下:
[数据类型][const][*][常量指针名称];
例如,int const *p; const int *p;
需要注意的是,指针常量强调的是指针的不可改变性,而常量指针强调的是指针对其所指对象的不可改变型,它所指向的对象的值是不能通过常量指针来改变的。
20 C++函数传递参数的方式有哪些
1)值传递
当进行值传递时,就是将实参的值复制到形参中,而形参和实参不是同一个存储单元,所以函数调用结束后,实参的值不会改变。
2)指针传递(实际上指针的值还是没有改变的,改变的只是指针中存放的地址所指向的变量,如果要改变指针的值,需要传递指针的引用或者指向指针的指针)
当进行指针传递时,形参是指针变量,实参是一个变量的地址,调用函数时,形参(指针变量)指向实参变量单元。这种方式还是“值传递”,只不过实参的值是变量的地址而已。而在函数中改变的不是实参的值,而是实参中存放的地址所指向的变量的值。
3)传引用
实参地址传递到形参,使形参的地址取实参的地址,从而使形参与实参共享同一单元的方式。
21 重载与覆盖有什么区别?
22 是否可以通过绝对内存地址进行参数赋值与函数调用
23 默认构造函数是否可以调用单参数构造函数
默认构造函数不可以调用单参数的构造函数。
例如:
class A
{
public:
A()
{
A(0);
print();
}
A(int j):i(j)
{
cout<<"call A(int j)"<<endl;
}
void print()
{
cout<<"call print()"<<endl;
}
int i;
};
此时i的值是未定义的。以上代码希望默认构造函数调用带参数的构造函数,可是却未能实现。因为在默认构造函数内部调用带餐的构造函数属于用户的行为而非编译器行为,它只执行函数调用,而不会执行其后的初始化表达式。只有生成对象时,初始化表达式才会随相应的构造函数一起调用。
可以使用委托构造函数class A
{
public:
A():A(0)
{
print();
}
A(int j):i(j)
{
cout<<"call A(int j)"<<endl;
}
void print()
{
cout<<"call print()"<<endl;
}
int i;
};
或者使用placement new
class A
{
public:
A()
{
printf("In A::(). m_x=%d\n", m_x);
new(this) A(0);
printf("Out A::(). m_x=%d\n", m_x);
}
A(int x)
{
printf("In A::(int x). x=%d\n", x);
m_x=x;
}
private:
int m_x;
};
24 什么是可重入函数?C语言如何写可重入函数
可重入函数是指能够被多个线程“同时”调用的函数,并且能保证函数结果正确性的函数。
在C语言中编写可重入函数时,尽量不要使用全局变量或静态变量,如果使用了全局变量或静态变量,就需要特别注意对这类变量访问的互斥。一般采用以下几种措施来保证函数的可重入性:信号量机制、关调度机制、关中断机制等方式。
需要注意的是,不可调用不可重入函数,当调用了不可重入的函数时,会使该函数也变为不可重入的函数。一般驱动程序都是不可重入的函数,因此在编写驱动程序时一定要注意重入的问题。
25 C语言中各种变量的默认初始值是什么?
全局变量放在内存的全局数据区,由编译器建立,如果在定义的时候不做初始化,则系统将自动为其初始化,数值型为0,字符型为NULL,即0,指针数组也被赋值为NULL。静态变量的情况与全局变量类型。而非静态局部变量如果不显示初始化,那么其内容是不可预料的,将是随机数,会很危险。
26 编译和链接的区别
27 编译型语言和解释性语言的区别
28 面向对象的基本特征:
封装是指将客观事物抽象成类,每个类有自己的数据和行为实现保护。
继承可以使用现有类的所有功能,而不需要重新编写原来的类,它的目的是为了进行代码复用和支持多态。
多态是指同一个实体同时具有多种形式,它主要体现在类的继承体系中,它是将父对象设置成为一个或更多的它的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。
29 复制构造函数与赋值运算符的区别是什么?
主要有以下3个方面的不同:
1)复制构造函数生成新的类对象,而赋值运算符不能。
2)由于复制构造函数是直接构造一个新的类对象,所以在初始化这个对象之前就不用检验源对象是否和新建对象相同。而赋值运算符总则需要这个操作,另外赋值运算符中如果原来的对象中有内存分配,要先把内存释放掉。
3)当类中有指针类型的成员变量时,一定要重写复制构造函数和赋值构造函数,不能使用默认的。
30 基类的构造函数/析构函数是否能被派生类继承
基类的构造函数/析构函数不能被派生类继承。
基类的构造函数不能被派生类继承,派生类中需要声明自己的构造函数。在设计派生类的构造函数时,不仅要考虑派生类所增加的数据成员初始化,也要考虑基类的数据成员的初始化。声明构造函数时,只需要对本类中新增成员进行初始化,对继承来的基类成员的初始化,需要调用基类构造函数完成。
基类的析构函数也不能被派生类继承,派生类需要自行声明析构函数。声明方法与一般类的析构函数一样,不需要显式地调用基类的析构函数,系统会自动隐式调用。需要注意的是,析构函数的调用次序与构造函数相反。
31 初始化列表和构造函数初始化的区别是什么?
初始化列表的一般形式如下:
Object::Object(int _x,int _y):x(_x),y(_y) {}
构造函数初始化一般通过构造函数实现,实现如下:
Object::Object(int _x,int _y)
{
x=_x;
y=_y;
}
上面的构造函数使用初始化列表的会显式地初始化类的成员;而没有使用初始化列表的构造函数是对类的成员赋值,并没有进行显式的初始化。
初始化和赋值对内置类型的成员没有什么的的区别,在成员初始化列表和构造函数体内进行,在性能和结果上都是一样的。对非内置类型成员变量,因为类类型的数据成员的数据成员对象在进入函数体前已经构造完成,也就是说在成员初始化列表处进行构造对象的工作,调用构造函数,在进入函数体之后,进行的是对已经构造好的类对象的赋值,又调用一个赋值赋值操作符才能完成(如果并未提供,则使用编译器提供的默认成员赋值行为)。为了避免两次构造,推荐使用类构造函数初始化列表。
但有很多场合必须使用带有初始化列表的构造函数。例如,成员类型是没有默认构造函数的类,若没有提供显示初始化时,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试调用默认构造函数将会失败。再例如const成员或者引用类型的成员,因为const对象或引用类型只能初始化,不能对它们进行赋值。
32 类的成员变量的初始化顺序
变量的初始化顺序:
1)基类的静态变量或全局变量。
2)派生类的静态变量或全局变量
3)基类的成员变量
4)派生类的成员变量
构造的顺序:
虚基类的构造函数->一般基类构造函数的调用(根据声明的次序调用每一个基类的构造函数)->如果存在虚函数表,设定vptr的值->对构造函数初始列表中的其他成员进行构造->如果存在对象成员分别调用其构造函数进行构造->初始化列表中的成员按照其在类中的声明次序进行构造->执行构造函数体内的代码
33 C++能设计实现一个不能被继承的类
C++不同于Java,Java中被final关键字修饰的类不能被继承。C++能实现不能继承的类,但是需要自己实现。
为了使类不被继承,最好的办法是使子类不能构造父类的部分,此时子类就无法实例化整个子类。在C++中,子类的构造函数会自动调用父类的构造函数,子类的析构函数也会自动调用父类的析构函数,所以只要把类的构造函数和析构函数都定义为private函数,那么当一个类试图从它那儿继承时,必然会由于试图调用构造函数、析构函数而导致编译错误,此时该类不能被继承。
可是这个类的构造函数和析构函数都是私有函数了,我们怎样才能得到该类的实例呢?这难不倒我们,我
们可以通过定义静态来创建和释放类的实例。基于这个思路,我们可以写出如下的代码:
class FinalClass1
{
public :
static FinalClass1* GetInstance()
{
return new FinalClass1;
}
static void DeleteInstance( FinalClass1* pInstance)
{
delete pInstance;
pInstance = 0;
}
private :
FinalClass1() {}
~FinalClass1() {}
};
这个类在基本上就能实现不能继承的功能。但是每次如果你都用这样一个类的话,估计你到最后不是你的程序崩溃了,而是你自己崩溃的更早。
因此,我们这样设计。
class CFinalClassMixin
{
friend class CParent;
private:
CFinalClassMixin(){}
~CFinalClassMixin(){}
};
class CParent: public CFinalClassMixin
{
public:
CParent(){}
~CParent(){}
};
class CChild : public CParent
{
};
但是发现没有用,想一想也是,CChild构造函数调用CParent的构造函数,而CParent的构造函数再调用CFinalClassMixin的构造函数,很显然是合法的。
我估计你也想骂了,唧唧歪歪讲了这么就还是不行。
但是请你想想,如果我是在CChild的构造函数直接调用CFinalClassMixin的构造函数,而CFinalClassMixin的构造函数是private,不能被调用,那我们岂不是达到了目的,但是我们如何才能在CChild中直接调用CFinalClassMixin的构造函数而不是通过CParent去调用了。
给你一分钟去想想。。。。。。。。。。。。。。
哈哈虚继承,虚继承刚好可以实现上述目的。
因此:
class CFinalClassMixin
{
friend class CParent;
private:
CFinalClassMixin(){}
~CFinalClassMixin(){}
};
class CParent: virtual public CFinalClassMixin
{
public:
CParent(){}
~CParent(){}
};
34 构造函数没有返回值,那么如何得知对象是否构造成功?
这里的“构造”不是单指分配对象本身的内存,而是指建立对象时做的初始化(如打开文件、连接数据库)
因为构造函数没有返回值,所以通知对象的构造失败的唯一方法就是在构造函数中抛出异常。构造函数中抛出异常将导致对象的析构函数不被执行,但对象发生部分构造时,已经构造完毕的子对象将会逆序地被析构。
35 C++中的空类默认产生哪些成员函数
C++中空类默认会产生以下6个函数:默认构造函数、复制构造函数、析构函数、赋值运算符重载函数、取址运算符重载函数、const取址运算符重载函数等。
class Empty
{
public:
Empty();//默认构造函数
Empty(const Empty&);//复制构造函数
~Empty(); //析构函数
Empyt& operator=(const Empty&); //赋值运算符
Empty* operator&(); //取址运算符
const Empty* operator&() const;//取址运算符const
};
35 C++提供默认值参数的函数
注意:
1)如果一个函数中有过个默认值,则形参分布中,默认参数应从右至左逐渐定义。
2)在默认参数调用时,调用顺序为从左至右逐个调用
3)默认值可以是全局变量、全局常量,甚至可以是一个函数,默认值不能是局部变量
4)默认参数可将一系列简单的重载函数合成为一个
36 实现多态的基本原理
应在构造函数中实现虚函数表的创建和虚函数指针的初始化。根据构造函数的调用顺序,在构造子类对象时,先调用父类的构造函数,此时编译器只“看到了”父类,并不知道后面是否还有继承,它初始化父类对象的虚函数表的指针,该虚函数表指针指向父类的虚函数表。当执行子类的构造函数时,子类对象的虚函数表指针被初始化,指向自身的虚函数表。
编译器发现一个类中有虚函数,便会立即为此类生成虚函数表,虚函数表的各表项为指向对应虚函数的指针。编译器还会在此类中隐含插入一个vptr指向虚函数表。调用此类的构造函数时,在类的构造函数中,编译器会隐含执行vptr与vtable的关联代码,将vptr指向对应的vtable,将类与此类的vtable联系起来,另外在调用类的构造函数时,指向基础类的指针此时已经变成指向具体类的this指针,这样依靠此this指针即可得到正确的vatble。这样才能真正与函数体进行连接,这就是动态联编,实现多态的基本原理。
37 C++中的多态种类有哪几种?
C++中的多态包括参数多态、引用多态、过载多态和强制多态等。
参数多态是指采用参数化模板,通过给定不同的类型参数,使得一个结构有多种类型、模板。
引用多态是指同样的操作可以用于一个类型及其子类型。
过载多态是指同一个名字在不同的上下文中有不同的类型。
强制多态则是指把操作对象的类型强加以变换,以符合或操作符的要求。
38什么函数不能声明为virtual
有5种情况:
1)只有类的成员函数才能说明为虚函数
2)静态成员不能为虚函数,因为调用静态成员函数不要实例,但调用虚函数需要从一个实例中指向虚函数表的指针以得到函数的地址,因此调用虚函数需要一个实例,两者互相矛盾。
3)内联函数不能为虚函数
4)构造函数不能为虚函数
5)析构函数可以为虚函数,而且通常声明为虚函数
构造函数不能是虚函数,是因为构造函数是在对象完全构造之前运行的,换句话说,运行构造函数前,对象还没有生成,更谈不上动态类型了。构造函数是初始化虚表指针,而虚函数放在虚表里面,当要调用虚函数的时候首先要知道虚表指针,这个就是矛盾的地方了,所以构造函数不可能是虚函数。一般上,构造函数是不能调用虚函数,但是在构造函数中还是可以调用虚函数,只是此时的虚函数不会表现动态类型,而只是静态类型。
39 是否可以把每个函数都声明为虚函数
虽然虚函数很有效,但是不能把每个函数都声明为虚函数。因为使用虚函数是要付出代价的。由于每个虚函数的对象在内存中都必须维护一个虚函数表指针,因此在使用虚函数时,尽管带来了方便,却会额外产生一个系统开销。
40 C++如何阻止一个类被实例化
C++中可以通过使用抽象类或者将构造函数声明为private阻止一个类被实例化。
41 C++哪些函数只能使用成员初始化列表而不能使用赋值。
在C++赋值与初始化列表的情况不一样,只能用初始化列表而不能用赋值的情况一般有一下3种:
1)当类中含有const(常量)、reference(引用)成员变量时,只能初始化不能对他们进行赋值。常量不能被赋值,只能被初始化,所以必须在初始化列表中完成,C++的引用也一定要初始化,所以必须在初始化列表中完成。
2)基类的构造函数都需要初始化列表。构造函数的意思是先开辟空间然后为其赋值,只能算是赋值,不算初始化。
3)成员类型是没有默认构造函数的类。若没有提供显式初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。
42 虚函数
指向基类的指针在操作它的多态类对象时,会根据不同的类对象调用其相应的函数,这个函数就是虚函数。虚函数使用virtual修饰函数名。虚函数的作用是在程序的运行阶段动态地选择合适的成员函数,在定义了虚函数后,可以在基类的派生类中对虚函数进行重新定义。在派生类中重新定义的函数应与虚函数具有相同的形参个数和形参类型,以实现统一的接口。如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。
在使用虚函数时要注意以下几方面:
1)只需要在声明函数的类体中使用关键字virtual将函数声明为虚函数,而定义函数时不需要使用关键字virtual。
2)当将基类中的某一成员函数声明为虚函数后,派生类中的同名函数自动成为虚函数。
3)如果声明了某个成员函数为虚函数,则在该类中不能再出现与这个成员函数同名并返回值、参数个数、类型都相同的非虚函数。在以该类为基类的派生类中,也不能出现这种同名函数。
4)非类的成员函数不能定义为虚函数,全局函数以及类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但可以讲析构函数定义为虚函数。将基类的析构函数定义为虚函数后,当利用delete删除一个指向派生类定义的对象指针时,系统会调用相应的类的析构函数。而不将析构函数定义为虚函数时,只调用基类的析构函数。
5)普通派生类对象,先调用基类构造函数再调用派生类构造。
6)基类的析构函数应该定义为虚函数,这样可以在实现多态的时候不造成内存泄露。基类析构函数未声明virtual,基类指针指向派生类时,delete指针不调用派生类析构函数。有virtual,则先调用派生类析构函数再调用基类析构。
7)基类指针动态建立派生类对象,普通调用派生类构造函数
8)指针声明不调用构造函数。
写出float x 与“零值”比较的if语句
请写出 float x 与“零值”比较的 if 语句:
const float EPSINON = 0.00001;
if ((x >= - EPSINON) && (x <= EPSINON)
不可将浮点变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”此类形式。
EPSINON应该是一个很小的值吧 因为计算机在处理浮点数的时候是有误差的,所以判断两个浮点数是不是相同,是要判断是不是落在同一个区间的,这个区间就是 [-EPSINON,EPSINON] EPSINON一般很小,10的-6次方以下吧,具体的好像不确定的,和机器有关。
以下内容引用自林锐《高质量C/C++代码编写指南》
4.3.3 浮点变量与零值比较
? 【规则4-3-3】不可将浮点变量用“==”或“!=”与任何数字比较。
千万要留意,无论是float还是double类型的变量,都有精度限制。所以一定要避免将浮点变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。
假设浮点变量的名字为x,应当将
if (x == 0.0) // 隐含错误的比较
转化为
if ((x>=-EPSINON) && (x<=EPSINON))
其中EPSINON是允许的误差(即精度)。