[C++]面试题(二)

26,C++中哪些函数不能被声明为虚函数?
答:普通函数(非成员函数),构造函数,内联成员函数、静态成员函数、友元函数。
(1)虚函数用于基类和派生类,普通函数所以不能
(2)构造函数不能是因为虚函数采用的是虚调用的方法,允许在只知道部分信息的情况的工作机制,
特别允许调用只知道接口而不知道对象的准确类型的方法,但是调用构造函数即使要创建一个对象,
那势必要知道对象的准确类型。
(3)内联成员函数的实质是在调用的地方直接将代码扩展开
(4)继承时,静态成员函数是不能被继承的,它只属于一个类,因为也不存在动态联编等
(5)友元函数不是类的成员函数,因此也不能被继承

27, 数组int c[3][3]; 为什么c,*c的值相等,(c+1),(*c+1)的值不等, c,*c,**c,代表什么意思?
答:c是第一个元素的地址,*c是第一行元素的首地址,其实第一行元素的地址就是第一个元素的地址,
**c是提领第一个元素。 为什么c,*c的值相等?
c: 数组名;是一个二维指针,它的值就是数组的首地址,也即第一行元素的首地址(等于 *c),
也等于第一行第一个元素的地址( & c[0][0]);可以说成是二维数组的行指针。
*c: 第一行元素的首地址;是一个一维指针,可以说成是二维数组的列指针。
**c:二维数组中的第一个元素的值;即:c[0][0]
所以:c 和 *c的值是相等的,但他们两者不能相互赋值,(类型不同)
(c + 1) :c是行指针,(c + 1)是在c的基础上加上二维数组一行的地址长度,
即从&c[0][0]变到了&c[1][0];
(*c + 1):*c是列指针,(*c + 1)是在*c的基础上加上二数组一个元素的所占的长度,
&c[0][0]变到了&c[0][1],从而(c + 1)和(*c + 1)的值就不相等了。

28,定义 int **pa[4][3],则变量pa占有的内存空间是多少?
答:int **p,在32位机器上 sizeof(p) = 4;
总共占有4*3*sizeof(p) = 48.

29,拷贝构造函数相关问题,深拷贝,浅拷贝,临时对象等
答:在C++中,三种对象需要拷贝的情况:一个对象以值传递的方式传入函数体,
一个对象以值传递的方式从函数返回,一个对象需要通过另外一个对象进行初始化。
执行先父类后子类的构造,对类中每一个数据成员递归地执行成员拷的动作.
深拷贝:如果一个类拥有资源,深拷贝意味着拷贝了资源和指针
浅拷贝:如果对象存在资源,而浅拷贝只是拷贝了指针,没有拷贝资源,
这样使得两个指针指向同一份资源,造成对同一份析构两次,程序崩溃。
临时对象的开销比局部对象小些。

临时对象:辅助一个表达式的计算 a + b + c ,或者间接构造的实参,函数返回非引用的时候,
都可能产生临时对象,临时对象生命周期,是单个语句,是右值。
临时对象的开销比局部对象小些。

30,指针和引用有什么分别;
答:引用必须初始化,即引用到一个有效的对象;而指针在定义的时候不必初始化,
可以在定义后面的任何地方重新赋值。
引用初始化后不能改变,指针可以改变所指的对象
不存在指向NULL的引用,但存在指向NULL的指针
引用的创建和销毁并不会调用类的拷贝构造函数
语言层面,引用的用法和对象一样;在二进制层面,引用一般都是通过指针来实现的,
只不过编译器帮我们完成了转换.引用既具有指针的效率,又具有变量使用的方便性和直观性.

31,写一个”标准”宏MIN,这个宏输入两个参数并返回较小的一个
答:面试者注意谨慎将宏定义中的“参数”和整个宏用括号括起来

#define      MIN(A, B)     ((A) <= (B)? (A):(B))

32,用一个宏定义FIND求一个结构体struc中某个变量相对struc的偏移量
答: #define FIND(struc, e) (size_t)&( ((struc*)0)->e )
解析:其中(struc*)0表示将常量0转化为struc*类型指针所指向的地址。
&( ((struc*)0)->e )表示取结构体指针(struc*)0的成员e的地址,因为该结构体的首地址为0,
所以其实就是得到了成员e距离结构体首地址的偏移量,(size_t)是一种数据类型,为了便于不同系统之间的移植,
最好定义为一种无符号型数据,一般为unsigned int

33,解析sizeof 以及 结构体的对齐问题
答:(1)sizeof(type),用于数据类型;
sizeof(var_name)或sizeof var_name用于变量 
sizeof操作符不能用于函数类型,不完全类型或位字段。
不完全类型指具有未知存储大小的数据类型,如未知存储大小的数组类型、未知内容的结构或联合类型、void类型等。
如int max(), char char_v [MAX]且MAX未知 , void类型
那么sizeof(max),sizeof(char_v),sizeof(void)都是错误的

当sizeof的参数为数组或者指针时
int a[50]; //sizeof(a)=4*50=200; 求数组所占的空间大小
int *a=new int[50];// sizeof(a)=4; a为一个指针,sizeof(a)是求指针
当sizeof的参数为结构或类时候
结构或者类中的静态成员不对结构或者类的大小产生影响,因为静态变量的存储位置 。
与结构或者类的实例地址无关。没有成员变量的结构或类的大小为1,
因为必须保证结构或类的每一 实例在内存中都有唯一的地址

(2)class MyStruct{ double ddal; char dda; int type;}
在VC中测试上面结构的大小时,你会发现sizeof(MyStruct)为16。
其实,这是VC对变量存储的一个特殊处理。为了提高CPU的存储速度,VC对一些变量的起始
地址做了“对齐”处理。在默认情况下,VC规定各成员变量存放的起始地址相对于结构的
始地址偏移量必须为该变量的类型占用字节数的倍数,如Char偏移量为sizeof(char)即1的倍数

先为第一个成员dda1分配空间,其起始地址跟结构的起始地址相同,偏移量0刚好为sizeof(double)的倍数,
该成员变量占用sizeof(double)=8个字节;接下来为第二个成员dda分配空间,这时
下一个可以分配的地址对于结构的起始地址的偏移量为8,是sizeof(char)的倍数,占sizeof(char)=1字节
为第三个成员type分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为9
,不是sizeof(int)=4的倍数,为了满足对齐方式对偏移量的约束问题,VC自动填充3个字节
这时下一个可以分配的地址对于结构的起始地址的偏移量是12,刚好是sizeof(int)=4的倍数,
所以把type存放在偏移量为12的地方,占 用sizeof(int)=4个字节。总的占用的空间大
小为:8+1+3+4=16,刚好为结构的字节边界数(即结构中占用最大空间的类型所占用的字节
数sizeof(double)=8)的倍数,所以没有空缺的字节需要填充。

34,在main函数执行之前,还会执行什么代码和工作
答:运行全局构造器,全局对象的构造函数会在main函数之前执行
设置栈指针,初始化static静态和global全局变量,即数据段的内容
将未初始化部分的赋初值:数值型short,int,long等为0,bool为FALSE,指针为NULL等
将main函数的参数,argc,argv等传递给main函数

35,如何判断一段程序是由C 编译程序还是由C++ 编译程序编译的?
答:C++ 编译时定义了 __cplusplus
C 编译时定义了 STDC

36,分别写出BOOL,int, float, 指针类型的变量 a 与 “零值”的比较语句
答:

  BOOL: if(!aor   if(a)
    int : if( 0 == a)
  float : const EXPRESSION EXP = 0.000001;
  if(a < EXP && a > -EXP)
 pointer:     if(a != NULL) or  if(a == NULL)

37,已知String类定义如下,尝试写出类的成员函数实现

class{
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& 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;
}
~String()
{
    delete[]  m_data;
}

38,论述C++类继承的优缺点
答:一,优点:类继承是在编译时刻静态定义的,可以直接使用,类继承可以较方便的改变从父类继承的实现
二,缺点:1,因为继承在编译时刻就定义了,所以无法在运行时刻改变从父类继承的实现
2,父类通常至少定义了子类的部分行为,父类的任何改变都可能影响到子类的行为
3,如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换
这种依赖关系先限制了灵活性并最终限制了复用性

39,运算符重载的三种方式和不允许重载的5个运算符
答:运算符重载意义是为了对用户自定义数据的操作和内定义的数据类型的操作形式一致
(1)普通函数,友元函数,类成员函数
(2).*(成员指针访问运算符)
::(域运算符)
sizeof 长度运算符
?:条件运算符
.(成员访问运算符)
40,友元关系有什么特性?
答:单向的,非传递的, 不能继承的.

41,理解析构函数和虚函数的用法和作用?
答:析构函数也是特殊的类成员函数,它没有返回类型,没有参数,不能随意调用,也没有重载。
在类对象生命期结束的时候,由系统自动调用释放在构造函数中分配的资源。
析构函数一般在对象撤消前做收尾工作,比如回收内存等工作。

虚函数的功能是使子类可以用同名的函数对父类函数进行重载,并且在调用时自动调用子类重载函
数,在基类中通过使用关键字virtual来声明一个函数为虚函数,该函数的功能可能在将来的派生类
中定义或者在基类的基础上扩展,系统只能在运行阶段才能动态的决定调用哪一个函数,动态的多态性,
如果是纯虚函数,则纯粹是为了在子类重载时有个统一的命名而已。

42,关键字volatile有什么含意?并给出三个不同的例子
答:一个定义为volatile的变量是说这变量可能会被意想不到地改变,编译器就不会去假设这个变量的值了。
精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值
而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:
1) 并行设备的硬件寄存器(如:状态寄存器)
2) 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3) 多线程应用中被几个任务共享的变量

深究:一个参数既可以是const还可以是volatile,一个例子是只读的状态寄存器,
它是volatile因为它可能被意想不到地改变,是const因为程序不应该试图去修改它。
一个指针可以是volatile,一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。

43,动态连接库的两种方式?
答:调用一个DLL中的函数有两种方法:
1.载入时动态链接(load-time dynamic linking),模块非常明确调用某个导出函数
,使得他们就像本地函数一样。这需要链接时链接那些函数所在DLL的导入库,导入库向
系统提供了载入DLL时所需的信息及DLL函数定位。
2.运行时动态链接(run-time dynamic linking),运行时可以通过LoadLibrary或Loa
dLibraryEx函数载入DLL。DLL载入后,模块可以通过调用GetProcAddress获取DLL函数的
出口地址,然后就可以通过返回的函数指针调用DLL函数了。如此即可避免导入库文件了。

44,C和C++有什么不同?
答:从机制上:c是面向过程的。c++是面向对象的,提供了类。c++编写面向对象的程序比c容易。
从适用的方向:c适合要求代码体积小的,效率高的场合,如嵌入式;c++适合更上层的,复杂的;
llinux核心大部分是c写的,因为它是系统软件,效率要求极高。
C语言是结构化编程语言,C++是面向对象编程语言
C++侧重于对象而不是过程,侧重于类的设计而不是逻辑的设计。

45,C++编译器自动为类产生的四个确缺省函数是什么?
答:默认构造函数,拷贝构造函数,析构函数,赋值函数

46,简单描述Windows内存管理的方法。
答:程序运行时需要从内存中读出这段程序的代码,代码的位置必须在物理内存中才能被运行,
由于现在的操作系统中有非常多的程序运行着,内存中不能够完全放下,所以引出了虚拟内存的概念。
把哪些不常用的程序片断就放入虚拟内存,当需要用到它的时候在load入主存(物理内存)中。
内存管理也计算程序片段在主存中的物理位置,以便CPU调度。

内存管理有块式管理,页式管理,段式和段页式管理。现在常用段页式管理
块式管理:把主存分为一大块、一大块的,当所需的程序片断不在主存时就分配一块主存空间,
把程 序片断load入主存,就算所需的程序片度只有几个字节也只能把这一块分配给它。
这样会造成很大的浪费,平均浪费了50%的内存空间,但时易于管理。
页式管理:把主存分为一页一页的,每一页的空间要比一块一块的空间小很多,显然这种方法
的空间利用率要比块式管理高很多
段式管理:把主存分为一段一段的,每一段的空间又要比一页一页的空间小很多,
这种方法在空间利用率上又比页式管理高很多,但是也有另外一个缺点。一个程序片断可能会被分为几十段,
这样很多时间就会被浪费在计算每一段的物理地址上,计算机最耗时间的大家都知道是I/O吧
段页式管理:结合了段式管理和页式管理的优点。把主存分为若干页,每一页又分为若干段,好处就很明显

47,Linux有内核级线程吗?
答:线程通常被定义为一个进程中代码的不同执行路线。从实现方式上划分,线程有两种类型:
“用户级线程”和“内核级线程”。 用户线程指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,
应用进程利用线程库提供创建、同步、调度,和管理线程的函数来控制用户线程。内核级线程需要内核的参与,
由内核完成线程的调度。其依赖于操作系统核心,由内核的内部需求进行创建和撤销。

用户线程不需要额外的内核开支,并且用户态线程的实现方式可以被定制或修改以适应特殊应用的要求,
但是当一个线程因 I/O 而处于等待状态时,整个进程就会被调度程序切换为等待状态,其他线程得不
到运行的机会;而内核线程则没有这个个限制,有利于发挥多处理器的并发优势,但却占用了更多的系统开支。

48,main 主函数执行完毕后,是否可能会再执行一段代码,给出说明?
答:可以,可以用_onexit 注册一个函数,它会在main 之后执行int fn1(void), fn2(void), fn3(void), fn4 (void)

49, i++ 相比 ++i 哪个更高效?为什么?
答:
(1)++i 比 i++效率高。
(2)i++要多调用一次类的构造和析够函数

50,windows平台下网络编程有哪几种网络编程模型?
答:有阻塞,select,基于窗体的事件模型,事件模型,重叠模型,完成端口模型。
除了阻塞模型外,其他都是非阻塞模型,其中效率最高的是完成端口模型,尤其在windows下服务器最合适了。
做客户端一般用事件模型了,select在window和类unix都可以使用。

51,什么是函数模板
答:函数模板技术定义了参数化的非成员函数,使得程序能够使用不同的参数类型调用相同的函数,而至于是何种类型,
则是由编译器确定从模板中生成相应类型的代码。编译器确定了模板函数的实际类型参数,称之为模板的实例化。

template<class T>定义模板标识
T Add(T a, T b)         //函数模板
{
   T result = a + b;
   return a + b;    //将两个参数使用“+”运算符进行运算,这两个参数并不知道是何种类型
}

该函数与一般函数的不同之处在于没有明确指出使用何种数据类型和返回值又是哪一种类型
如何在程序中调用该函数

#include<iostream>  //包含标准输入输出头文件
#include<string>   //C++中的字符串处理头文件
using namespace std;
template<class T>
T Add(T a, T b)         //函数模板
{
   T result = a + b;
   return a + b;    //将两个参数使用“+”运算符进行运算,这两个参数并不知道是何种类型
}
int main(int argc, char* argv[])
{
    cout<<"2+3="<<Add(2,3)<<endl;  //输出整形的+运算结果
   cout<<"sdf+123="<<Add(string("sdf"), string("123"))<<endl;
  return 0;
}

52,什么是类模板
答:描述了能够管理其他数据类型的通用数据类型,通常用于建立包含其他类型的容器类
对于这些容器,无论是哪一种数据类型,其操作方式是一样的,但是针对具体的类型又是专用的,

template<class T>
class TemplateSample
{
   private:
    T& emtity;            //使用参数类型成员
    public:
  void F(T& arg);        //使用参数类型定义成员函数
}
`
该示例定义了一个类模板,类模板中的模板形参T需要用户在使用的时候进行定义 
TemplateSampledemo; //针对该模板使用int类型 
demo.F(123); //调用类模板中的成员函数 
template

TemplateSample<int , char, 12>demo;      //使用非类类型的模板
#include<iostream>
template<class T, class T2, int num>
class CSampleTemplate
{
   private:
   T t1;
   T2 t2;
  public:
  CSampleTemplate(T arg1, T2 arg2)         //构造函数中使用模板参数
  {
      t1 = arg1 + num;
      t2 = arg2 + num;
  }
  void Write()
 {
  std::cout<<"t1:"<<t1<<"t2"<<t2<<endl;
 }

CSampleTemplate ()
{}
}
int main(int argc, char* argv[])
{
    CSampleTemplate<int, int, 3>temp(1,2);
    temp.Write();
    return 0;
}

53,什么是容器
答:STL是一个标准的C++库,容器只是其中一个重要的组成部分,有顺序容器和关联容器
1)顺序容器,指的是一组具有相同类型T的对象,以严格的线性形式组织在一起
包括vector, deque, list
2)关联容器,提供一个key实现对对象的随机访问,其特点是key是有序的元素是按照预定义的键顺序插入的i
set ,集合, 支持唯一键值,提供对键本身的快速检索,例如set:{学号}
set,多重集合,支持可重复键值,提供对键本身的快速检索,例如multiset:{姓名}
map

54,介绍关联容器
答:

#include<vector>     //包含头文件
using std::vector           //使用命名限定
vector<int>vInts;
创建一个Widget类型为空的vector对象
vector<Widget>vWidgets;                    //空的vector对象
vector<Widget>vWidgets(500);             //包含500个对象的vector
vector<Widget>vWidgets(500, Widget(0));       //包含500个对象的vector,并且初始化为0
vector<Widget>vWidgetFromAnother(vWeigets);        //利用现有的vector创建一个拷贝

向vector中添加一个数据,默认方式是push_back,表示将数据添加到vector的尾部,并且按照需要来分配内存,如

for(int i = 0 ; i < 10; i ++)
v.push_back(Widget(i));

如果想获取vector v的大小,但不知道它是否为空,或者已经包含了数据,可用如下代码实现

int nSize = v.empty()? -1:static_cast<int>(v.size());

访问vector中的数据有两种方法 vector::at()   和  vector::operator[],其中vector::at()进行了边界检查

vector<int>v;                  //定义了vector对象
v.reserve(10);                     //分配空间但是没有初始化
for(int i = 0 ; i < 7; i++)
{  v. push_back(i);}
int iVal1 = v[7];                    //不进行边界检查
int iVal2 = v.at(7);           //进行边界检查

deque容器是一个双端队列,存放的元素不是以连续的方式存放的
list容器是一种链表的实现,储存的元素是通过使用双向链表实现的

55,什么是迭代器的范围
答:迭代器是STL提供的对一个容器中的对象的访问方法,定义了容器中对象的范围,迭代器就如同一个指针。

vector<int>v;     //声明vector变量
v.push_back(2);    //插入数据
v.push_back(1);
vector<int>::iterator first = v.begin();      //获取vector<int>的一个元素的迭代器
while1(first != v.end())           //使用迭代器遍历vector,一直到最后一个元素
{
   int i = *first;     //获取迭代器指向的元素的值
first++;
}

55,C++如何实现泛型编程
答:泛型编程实现了于特定类型的操作算法,由编译器根据泛型的调用所传递的类及模板生成该类型专用的代码。

#include<iostream>
#include<string>
using namespaces std;
template<class T>
T Add(T a, T b)
{
    T result;           // 使用参数化的类型定义变量
   result = a + b;
  return result;
}
int main(int argc, char* argv[])
{
  cout<<"2+3="<<Add(2,3)<<endl;  
  cout<<"sdf+123="<<Add(string("sdf"), string("123"));
   return 0;
}

56,参数传递的方式和多态参数传递的实现
答:参数传递有传值,传指针,或者是引用等三种,下面做详细的介绍
1)传值方式适合一般的数值传递,并且不改变原数据,但是要消耗内存空间
2)传递指针方式适合传递数组和指针,由于传递的是地址,所以直接操作会改变原数据
3)引用方式和指针方式比较类似,是相对比较新的一种方式,一般情况下能用传地址的就能用引用
而且使用引用更方便一些
实现多态主要是采用指针和引用,传值方式是复制数据,其类型编译器就已经决定,而多态是类型要等到执行器才能决定,
所以不适用传值方式来实现多态参数传递

57,C++和C定义结构体区别是什么?
答:C++中的结构和类其实具备几乎一样的功能,结构体内也是可以声明函数,C++的结构体和类默认具有不一样的访问属性

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值