C++ 面试经典题目
1、描述内存分配方式以及它们的区别?
1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。
2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集。
3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活,但问题也最多。
2、试比较顺序存储结构和链式存储结构的优缺点。在什么情况下用顺序表比链表好? 答:① 顺序存储时,相邻数据元素的存放地址也相邻(逻辑与物理统一);要求内存中可用存储单元的地址必须是连续的。
优点:存储密度大(=1),存储空间利用率高。缺点:插入或删除元素时不方便。 ②链式存储时,相邻数据元素可随意存放,但所占存储空间分两部分,一部分存放结点值,另一部分存放表示结点间关系的指针
优点:插入或删除元素时很方便,使用灵活。缺点:存储密度小(<1),存储空间利用率低。 顺序表适宜于做查找这样的静态操作;链表宜于做插入、删除这样的动态操作。 若线性表的长度变化不大,且其主要操作是查找,则采用顺序表;
若线性表的长度变化较大,且其主要操作是插入、删除操作,则采用链表。
3、
4、 拷贝构造函数在哪几种情况下会被调用?
拷贝构造函数
拷贝构造函数是一个特殊的构造函数,其作用是用一个已经存在的对象初始化本类的新对象。每个类都有一个拷贝构造函数,它可以是根据用户的需要自定义,也可以由系统自动生成。拷贝构造函数名与类名相同,但参数是本类对象的引用。拷贝构造函数没有返回值。
定义拷贝构造函数的格式为:
类名(类名&对象名)
{
//函数体
}
其中,对象名是用来初始化另一个对象的对象的引用。
构造函数只在对象被创建时自动调用,而拷贝构造函数在下列三种情况下会被自动调用:
①用一个对象去初始化本类的另一个对象时。
②函数的形参是类的对象,在进行形参和实参的结合时。
③函数的返回值是类的对象,函数执行完返回时。
1、当用类的一个对象去初始化该类的另一个对象时。
例如:
int main()
{
Point A(1,2);
Point B(A); //用对象A初始化对象B,拷贝构造函数被调用
cout<<"B.GetX()<<endl;
return 0;
}
2、如果函数的形参是类的对象,调用函数时,进行形参和实参结合时。
例如:
void f(Point p)
{ cout<<p.GetX()<<endl; }
int main()
{
Point A(1,2);
f(A); //函数的形参为类的对象,当调用函数时,拷贝构造函数被调用
}
3、如果函数的返回值是类的对象,函数执行完成返回调用者时。
例如:
Point g()
{
Point A(1,2);
return A; //函数的返回值是类的对象,返回函数值时,调用拷贝构造函数
}
int main()
{
Point B;
B=g();
}
5. 什么时候必须重写拷贝构造函数?
答:当构造函数涉及到动态存储分配空间时,要自己写拷贝构造函数,并且要深拷贝。
6、拷贝构造函数和赋值函数的区别
一、拷贝构造,是一个的对象来初始化一边内存区域,这边内存区域就是你的新对象的内存区域赋值运算,对于一个已经被初始化的对象来进行operator=操作
class
A;
A a;
A b=a; //拷贝构造函数调用 //或 A
b(a); //拷贝构造函数调用
///////////////////////////////////
A a;
A b;
b =a; //赋值运算符调用
你只需要记住,在C++语言里,
String s2(s1);
String s3
= s1;
只是语法形式的不同,意义是一样的,都是定义加初始化,都调用拷贝构造函数。
二、
一般来说是在数据成员包含指针对象的时候,应付两种不同的处理需求的
一种是复制指针对象,一种是引用指针对象 copy大多数情况下是复制,=则是引用对象的
例子:
class
A
{
int nLen;
char * pData;
}
显然
A
a, b;
a=b的时候,对于pData数据存在两种需求
第一种copy
a.pData = new
char [nLen];
memcpy(a.pData, b.pData, nLen);
另外一种(引用方式):
a.pData = b.pData
通过对比就可以看到,他们是不同的 往往把第一种用copy使用,第二种用=实现 你只要记住拷贝构造函数是用于类中指针,对象间的COPY
三、
和拷贝构造函数的实现不一样
拷贝构造函数首先是一个构造函数,它调用的时候产生一个对象,是通过参数传进来的那个对象来初始化,产生的对象。 operator=();是把一个对象赋值给一个原有的对象,所以如果原来的对象中有内存分配要先把内存释放掉,而且还要检查一下两个对象是不是同一个对象,如果是的话就不做任何操作。
还要注意的是拷贝构造函数是构造函数,不返回值 ; 而赋值函数需要返回一个对象自身的引用,以便赋值之后的操作
6、关于类的构造函数,下面说法不正确的是(
C )
A.构造函数的作用是在对象被创建时将对象初始化为一个特定的状态
B.构造函数的函数名与类名相同
C.构造函数可以声明为虚函数
D.构造函数在对象被创建时被系统自动调用
常见的不能声明为虚函数的有:普通函数(非成员函数)、静态成员函数、内联成员函数、构造函数、友元函数。
l 为什么C++不支持普通函数为虚函数? 普通函数(非成员函数)只能overload,不能被override,声明为虚函数也没有什么意思,因此编译器会在编译时绑定函数。
l 为什么C++不支持构造函数为虚函数?这个原因很简单,主要是从语义上考虑,所以不支持。因为构造函数本来是为了明确初始化对象成员才产生的,然而virtual function主要是为了在不完全了解细节的情况下也能正确处理对象。另外,虚函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用虚函数来完成你想完成的动作。
l 为什么C++不支持静态成员函数为虚函数? 静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他不归某个对象所有,所以他也没有动态绑定的必要性。
l 为什么C++不支持内联成员函数为虚函数? 其实很简单,内联函数就是为了在代码中直接展开,减少函数调用话费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。再说,inline函数在编译时被展开,虚函数在运行时才能动态的绑定函数。
l 为什么C++不支持友元函数为虚函数?因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。
7、有关析构函数的说法中不正确的是()
A.析构函数有且只有一个
B.析构函数无任何类型
C.析构函数与构造函数一样可以有形参
D.析构函数的作用是在对象生命周期结束时收回先前分配的内存单元
构造函数可以被重载;
析构函数不可以被重载(重载的必要条件是参数的类型或者个数不一样或者顺序不一样,无参就不能重载)
8、c++函数同名不同返回值不算重载!函数重载是忽略返回值类型的。
成员函数被重载的特征有:
1) 相同的范围(在同一个类中);
2) 函数名字相同;
3) 参数不同;
4) virtual关键字可有可无。
l 成员函数的重载,其特点:
(1)在同一个类中;
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
(5)参数类型和个数相同,但顺序不同
l 覆盖是指派生类函数覆盖基类函数,其特点:
(1)位于派生类与基类中;
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
9、引用应用
l 引用作为参数
引用的一个重要作用就是作为函数的参数。以前的C语言中函数参数传递是值传递,如果有大块数据作为参数传递的时候,采用的方案往往是指针,因为 这样可以避免将整块数据全部压栈,可以提高程序的效率。但是现在(C++中)又增加了一种同样有效率的选择(在某些特殊情况下又是必须的选择),就是引 用。
void
swap(int &p1, int &p2) //此处函数的形参p1, p2都是引用
{ int p; p=p1; p1=p2; p2=p; }
(1)传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。
(2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给 形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效 率和所占空间都好。
(3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的 形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。
如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。
2、常引用
常引用声明方式:const 类型标识符 &引用名=目标变量名;
用这种方式声明的引用,不能通过引用对目标变量的值进行修改,从而使引用的目标成为const,达到了引用的安全性。
【例3】:
int
a ;
const int &ra=a;
ra=1; //错误
a=1; //正确
3、引用作为返回值
要以引用返回函数值,则函数定义时要按以下格式:
类型标识符 &函数名(形参列表及类型说明)
{函数体}
说明:
(1)以引用返回函数值,定义函数时需要在函数名前加&
(2)用引用返回一个函数值的最大好处是,在内存中不产生被返回值的副本。
【例5】以下程序中定义了一个普通的函数fn1(它用返回值的方法返回函数值),另外一个函数fn2,它以引用的方法返回函数值。
#include
<iostream.h>
float temp; //定义全局变量temp
float fn1(float r); //声明函数fn1
float &fn2(float r); //声明函数fn2
float fn1(float r) //定义函数fn1,它以返回值的方法返回函数值
{
temp=(float)(r*r*3.14);
return temp;
}
float &fn2(float r) //定义函数fn2,它以引用方式返回函数值
{
temp=(float)(r*r*3.14);
return temp;
}
void main() //主函数
{
float a=fn1(10.0); //第1种情况,系统生成要返回值的副本(即临时变量)
float &b=fn1(10.0); //第2种情况,可能会出错(不同 C++系统有不同规定)
//不能从被调函数中返回一个临时变量或局部变量的引用
float c=fn2(10.0); //第3种情况,系统不生成返回值的副本
//可以从被调函数中返回一个全局变量的引用
float &d=fn2(10.0); //第4种情况,系统不生成返回值的副本
//可以从被调函数中返回一个全局变量的引用
cout<<a<<c<<d;
}
引用作为返回值,必须遵守以下规则:
(1)不能返回局部变量的引用。这条可以参照Effective C++[1]的Item 31。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。
(2)不能返回函数内部new分配的内存的引用。这条可以参照Effective C++[1]的Item 31。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一 个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。
(3)可以返回类成员的引用,但最好是const。这条原则可以参照Effective C++[1]的Item 30。主要原因是当对象的属性是与某种业务规则(business rule)相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的非常 量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。
(4)引用与一些操作符的重载:
流操作符<<和>>,这两个操作符常常希望被连续使用,例如:cout << "hello" << endl; 因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。可选的其它方案包括:返回一个流对象和返回一个流对象指针。但是对于返回 一个流对象,程序必须重新(拷贝)构造一个新的流对象,也就是说,连续的两个<<操作符实际上是针对不同对象的!这无法让人接受。对于返回一 个流指针则不能连续使用<<操作符。因此,返回一个流对象引用是惟一选择。这个唯一选择很关键,它说明了引用的重要性以及无可替代性,也许这 就是C++语言中引入引用这个概念的原因吧。 赋值操作符=。这个操作符象流操作符一样,是可以连续使用的,例如:x = j = 10;或者(x=10)=100;赋值操作符的返回值必须是一个左值,以便可以被继续赋值。因此引用成了这个操作符的惟一返回值选择。
10、 对象.成员函数
对象 成员函数
对/错
1、 const
const 对
2、 const
non-const 错
3、 non-const
const 对
4、 not-const
non-const 对
成员函数调用成员函数
成员函数 成员函数
对/错
5、 const
const 对
6、 const
non-const 错
7、 non-const
const 对
8、 non-const
non-const 对
(1)const修饰成员变量
const修饰类的成员函数,表示成员常量,不能被修改,同时它只能在初始化列表中赋值。
class A
{
…
const
int nValue; //成员常量不能被修改
…
A(int
x): nValue(x) { } ; //只能在初始化列表中赋值
}
(2)const修饰成员函数
const修饰类的成员函数,则该成员函数不能修改类中任何非const成员函数。一般写在函数的最后来修饰。
class A
{
…
void function()const; //常成员函数, 它不改变对象的成员变量.
//也不能调用类中任何非const成员函数。
}
对于const类对象/指针/引用,只能调用类的const成员函数,因此,const修饰成员函数的最重要作用就是限制对于const对象的使用。
a. const成员函数不被允许修改它所在对象的任何一个数据成员。
b. const成员函数能够访问对象的const成员,而其他成员函数不可以。
(3)const修饰类对象/对象指针/对象引用
- const修饰类对象表示该对象为常量对象,其中的任何成员都不能被修改。对于对象指针和对象引用也是一样。
- const修饰的对象,该对象的任何非const成员函数都不能被调用,因为任何非const成员函数会有修改成员变量的企图。
11、c++中vector的用法总结
一、 定义和初始化
vector< typeName > v1; //默认v1为空,故下面的赋值是错误的v1[0]=5;只有存在的数据才可以用下标访问!
vector<typeName>v2(v1); 或v2=v1;或vector<typeName>
v2(v1.begin(), v1.end());//v2是v1的一个副本,若v1.size()>v2.size()则赋值后v2.size()被扩充为 v1.size()。
vector< typeName > v3(n,i);//v3包含n个值为i的typeName类型元素
vector< typeName > v4(n); //v4含有n个值为0的元素
int a[4]={0,1,2,3,3}; vector<int> v5(a,a+5);//v5的size为5,v5被初始化为a的5个值。后一个指针要指向将被拷贝的末元素的下一位置。
vector<int> v6(v5);//v6是v5的拷贝
vector< 类型 > 标识符(最大容量,初始所有值);
三、vector对象最重要的几种操作
1. v.push_back(t) 在容器的最后添加一个值为t的数据,容器的size变大。
另外list有push_front()函数,在前端插入,后面的元素下标依次增大。
注意:此处的t如果是数组类型,则可能造成每次添加数据后,所有数据被重新赋值为最新数据。因为t传递的是数组的地址,而不是数组的值。因此,应该定义字符串型的vector和t,即:
vector<string> text,而不上vector<char*> text
2. v.size() 返回容器中数据的个数,size返回相应vector类定义的size_type的值。v.resize(2*v.size)或
v.resize(2*v.size, 99) 将v的容量翻倍(并把新元素的值初始化为99)
3. v.empty() 判断vector是否为空
4. v[n] 返回v中位置为n的元素
5. v.insert(pointer,number, content) 向v中pointer指向的位置插入number个content的内容。
还有v. insert(pointer,
content),v.insert(pointer,a[2],a[4])将a[2]到a[4]三个元素插入。
6. v.pop_back() 删除容器的末元素,并不返回该元素。
7.v.erase(pointer1,pointer2) 删除pointer1到pointer2中间(包括pointer1所指)的元素。
vector中删除一个元素后,此位置以后的元素都需要往前移动一个位置,虽然当前迭代器位置没有自动加1,
但是由于后续元素的顺次前移,也就相当于迭代器的自动指向下一个位置一样。
8. v1==v2 判断v1与v2是否相等。
12.v.clear() 删除容器中的所有元素。12.v.clear() 删除容器中的所有元素。
12.请问下面的代码输出为多少?
#include <stdio.h> #define product(x) (x*x) int main() { int a=5,b=3; !a&&b++; printf("%d %d/n",a,b ); return 0; } |
答案为5,3,因为!a为假,故b++压根就不能执行了
下面这个程序的返回值是多少?
char foo(void) { unsigned a=6; int b=-20; char c; (a+b>6)?(c=1):(c=0); return c; } |
答案是1,因为a+b计算时b会被隐式转换为unsigned.(隐式转换)
13、
int i;
int *a = &i;//这里a是一个指针,它指向变量i
int &b = i;//这里b是一个引用,它是变量i的引用,引用是什么?它的本质是什么?下面会具体讲述
int * &c = a;//这里c是一个引用,它是指针a的引用
int & *d;//这里d是一个指针,它指向引用,但引用不是实体,所以这是错误的
int * &c = a; 怎么才用意理解呢? 变量前面的符号决定了它自身的类型(& 引用),类型与紧跟在类型后面的符号,决定了其指向的类型( int *) ---int 的 指针。
14、已知以下这些类型说明
Template<class elemType> class
Array;
enum statues{…};
typedef string *pstring;
下面哪一个对象的定义是错误的 3 分
A. Array<
pstring > aps(1024);
B. Array<
Array<int> > aai(1024);
C. Array<int
*&> pri(1024);
D. Array<
complex<double> >
acd(1024);
E. Array<
status > as(1024);
答案是C
1)类型int*&是一个reference,代表一个指针,由于reference必须在定义之际寝化,而Array template并未提供任何方法可以寝化pri所指的元素,所以这是一个错误的对象定义。
2)对象aai是一个Array,其元素是Arrays,后者的元素是ints.
3)对象acd是一个由complex组成的Array,复数的虚实两部的类型都是double.
4)对象as是一个Array,其元素类型为Status的枚举值.
5)对象aps是一个Array,其元素都是指针,指向string.