干货收藏:68道C语言与C++常见面试题(二)

18 请解析(*(void (*)( ) )0)( )的含义


void (*0)( ) :是一个返回值为void,参数为空的函数指针0。

(void (*)( ))0:把0转变成一个返回值为void,参数为空的函数指针。

*(void (*)( ))0:在上句的基础上加*表示整个是一个返回值为void,无参数,并且起始地址为0的函数的名字。

(*(void (*)( ))0)( ):这就是上句的函数名所对应的函数的调用。

19 C语言的指针和引用和c++的有什么区别?


指针有自己的一块空间,而引用只是一个别名;

使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;

作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象;

可以有const指针,但是没有const引用;

指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;

指针可以有多级指针(**p),而引用止于一级;

指针和引用使用++运算符的意义不一样;

如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

20 typedef 和define 有什么区别


用法不同:typedef 用来定义一种数据类型的别名,增强程序的可读性。define 主要用来定义 常量,以及书写复杂使用频繁的宏。

执行时间不同:typedef 是编译过程的一部分,有类型检查的功能。define 是宏定义,是预编译的部分,其发生在编译之前,只是简单的进行字符串的替换,不进行类型的检查。

作用域不同:typedef 有作用域限定。define 不受作用域约束,只要是在define 声明后的引用 都是正确的。

对指针的操作不同:typedef 和define 定义的指针时有很大的区别。

注意:typedef 定义是语句,因为句尾要加上分号。而define 不是语句,千万不能在句尾加分号。


21 指针常量与常量指针区别


指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变。常量指针 是指定义了一个指针,这个指针指向一个只读的对象,不能通过常量指针来改变这个对象的值。 指针常量强调的是指针的不可改变性,而常量指针强调的是指针对其所指对象的不可改变性。


注意:无论是指针常量还是常量指针,其最大的用途就是作为函数的形式参数,保证实参在被调用 函数中的不可改变特性。


22 简述队列和栈的异同


队列和栈都是线性存储结构,但是两者的插入和删除数据的操作不同,队列是“先进先出”,栈是 “后进先出”。


注意:区别栈区和堆区。堆区的存取是“顺序随意”,而栈区是“后进先出”。栈由编译器自动分 配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。堆一般由程序员 分配释放, 若程序员不释放,程序结束时可能由OS 回收。分配方式类似于链表。 它与本题中的堆和栈是两回事。堆栈只是一种数据结构,而堆区和栈区是程序的不同内存存储区域。


23 设置地址为0x67a9 的整型变量的值为0xaa66


int *ptr;

ptr = (int *)0x67a9;

*ptr = 0xaa66;

注意:这道题就是强制类型转换的典型例子,无论在什么平台地址长度和整型数据的长度是一样的, 即一个整型数据可以强制转换成地址指针类型,只要有意义即可。


24 编码实现字符串转化为数字


编码实现函数atoi(),设计一个程序,把一个字符串转化为一个整型数值。例如数字:“5486321 ”, 转化成字符:5486321。

int myAtoi(const char * str) 
{
   int num = 0; //保存转换后的数值 
   int isNegative = 0; //记录字符串中是否有负号 
 
   int n =0; 
   char *p = str; 
   if(p == NULL) //判断指针的合法性 
   { 
      return -1; 
   } 
   while(*p++ != '\0') //计算数字符串度 
   { 
      n++; 
   } 
   p = str; 
   if(p[0] == '-') //判断数组是否有负号 
   { 
      isNegative = 1; 
   } 
 
   char temp = '0'; 
   for(int i = 0 ; i < n; i++) 
   { 
      char temp = *p++; 
       if(temp > '9' ||temp < '0') //滤除非数字字符 
      { 
         continue; 
      } 
      if(num !=0 || temp != '0') //滤除字符串开始的0 字符 
      { 
         temp -= 0x30; //将数字字符转换为数值 
          num += temp *int( pow(10 , n - 1 -i) ); 
       } 
   } 
   if(isNegative) //如果字符串中有负号,将数值取反 
   { 
       return (0 - num); 
   } 
   else 
   { 
      return num; //返回转换后的数值 
   } 
} 

25 C语言的结构体和C++的有什么区别


C语言的结构体是不能有函数成员的,而C++的类可以有。

C语言的结构体中数据成员是没有private、public和protected访问限定的。而C++的类的成员有这些访问限定。

C语言的结构体是没有继承关系的,而C++的类却有丰富的继承关系。

注意:虽然C的结构体和C++的类有很大的相似度,但是类是实现面向对象的基础。而结构体只可以简单地理解为类的前身。


26 简述指针常量与常量指针的区别


指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变。常量指针是指定义了一个指针,这个指针指向一个只读的对象,不能通过常量指针来改变这个对象的值。

指针常量强调的是指针的不可改变性,而常量指针强调的是指针对其所指对象的不可改变性。

注意:无论是指针常量还是常量指针,其最大的用途就是作为函数的形式参数,保证实参在被调用函数中的不可改变特性。


27 如何避免“野指针”


指针变量声明时没有被初始化。解决办法:指针声明时初始化,可以是具体的地址值,也可让它指向NULL。

指针p被free或者delete之后,没有置为NULL。解决办法:指针指向的内存空间被释放后指针应该指向NULL。

指针操作超越了变量的作用范围。解决办法:在变量的作用域结束前释放掉变量的地址空间并且让指针指向NULL。

28 句柄和指针的区别和联系是什么?

句柄和指针其实是两个截然不同的概念。Windows系统用句柄标记系统资源,隐藏系统的信息。你只要知道有这个东西,然后去调用就行了,它是个32it的uint。指针则标记某个物理内存地址,两者是不同的概念。


29 new/delete与malloc/free的区别是什么


new能自动计算需要分配的内存空间,而malloc需要手工计算字节数。

int *p = new int[2];

int *q = (int *)malloc(2*sizeof(int));

new与delete直接带具体类型的指针,malloc和free返回void类型的指针。

new类型是安全的,而malloc不是。例如int *p = new float[2];就会报错;而int p = malloc(2sizeof(int))编译时编译器就无法指出错误来。

new一般分为两步:new操作和构造。new操作对应与malloc,但new操作可以重载,可以自定义内存分配策略,不做内存分配,甚至分配到非内存设备上,而malloc不行。

new调用构造函数,malloc不能;delete调用析构函数,而free不能。

malloc/free需要库文件stdlib.h的支持,new/delete则不需要!

注意:delete和free被调用后,内存不会立即回收,指针也不会指向空,delete或free仅仅是告诉操作系统,这一块内存被释放了,可以用作其他用途。但是由于没有重新对这块内存进行写操作,所以内存中的变量数值并没有发生变化,出现野指针的情况。因此,释放完内存后,应该讲该指针指向NULL。


30 说一说extern“C”


extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。


这个功能十分有用处,因为在C++出现以前,很多代码都是C语言写的,而且很底层的库也是C语言写的,为了更好的支持原来的C代码和已经写好的C语言库,需要在C++中尽可能的支持C,而extern "C"就是其中的一个策略。


C++代码调用C语言代码

在C++的头文件中使用

在多个人协同开发时,可能有的人比较擅长C语言,而有的人擅长C++,这样的情况下也会有用到

31 请你来说一下C++中struct和class的区别


在C++中,class和struct做类型定义是只有两点区别:


默认继承权限不同,class继承默认是private继承,而struct默认是public继承

class还可用于定义模板参数,像typename,但是关键字struct不能同于定义模板参数 C++保留struct关键字,原因

保证与C语言的向下兼容性,C++必须提供一个struct

C++中的struct定义必须百分百地保证与C语言中的struct的向下兼容性,把C++中的最基本的对象单元规定为class而不是struct,就是为了避免各种兼容性要求的限制

对struct定义的扩展使C语言的代码能够更容易的被移植到C++中

32 C++类内可以定义引用数据成员吗?


可以,必须通过成员函数初始化列表初始化。

33 C++中类成员的访问权限


C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员


34 什么是右值引用,跟左值又有什么区别?


左值和右值的概念:


左值:能取地址,或者具名对象,表达式结束后依然存在的持久对象;

右值:不能取地址,匿名对象,表达式结束后就不再存在的临时对象; 区别:

左值能寻址,右值不能;

左值能赋值,右值不能;

左值可变,右值不能(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数改变);

35 面向对象的三大特征


封装性:将客观事物抽象成类,每个类对自身的数据和方法实行 protection (private , protected , public )。

继承性:广义的继承有三种实现形式:实现继承(使用基类的属性和方法而无需额外编码的能力)、可 视继承(子窗体使用父窗体的外观和实现代码)、接口继承(仅使用属性和方法,实现滞后到子类实现)。

多态性:是将父类对象设置成为和一个或更多它的子对象相等的技术。用子类对象给父类对象赋值 之后,父类对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。

36 说一说c++中四种cast转换


C++中四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast


1、const_cast


用于将const变量转为非const

2、static_cast


用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;

3、dynamic_cast


用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的***对于指针返回NULL,对于引用抛异常***。要深入了解内部转换的原理。


向上转换:指的是子类向基类的转换

向下转换:指的是基类向子类的转换

它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。


4、reinterpret_cast


几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;

5、为什么不使用C的强制转换?


C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。

37 C++的空类有哪些成员函数


缺省构造函数。

缺省拷贝构造函数。

缺省析构函数。

缺省赋值运算符。

缺省取址运算符。

缺省取址运算符 const 。

注意:有些书上只是简单的介绍了前四个函数。没有提及后面这两个函数。但后面这两个函数也是 空类的默认函数。另外需要注意的是,只有当实际使用这些函数的时候,编译器才会去定义它们。


38 对c++中的smart pointer四个智能指针:shared_ptr,unique_ptr,weak_ptr,auto_ptr的理解


C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被11弃用。


智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。


auto_ptr(c++98的方案,cpp11已经抛弃)

采用所有权模式。


auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.”));

auto_ptr<string> p2;

p2 = p1; //auto_ptr不会报错.

此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题!


unique_ptr(替换auto_ptr)

unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。


采用所有权模式。


unique_ptr<string> p3 (new string ("auto"));   //#4

unique_ptr<string> p4;                       //#5

p4 = p3;//此时会报错!!

编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。


另外unique_ptr还有更聪明的地方:当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:


unique_ptr<string> pu1(new string ("hello world"));

unique_ptr<string> pu2;

pu2 = pu1;                                      // #1 not allowed

unique_ptr<string> pu3;

pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed

其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。


注:如果确实想执行类似与#1的操作,要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。例如:


unique_ptr<string> ps1, ps2;

ps1 = demo("hello");

ps2 = move(ps1);

ps1 = demo("alexia");

cout << *ps2 << *ps1 << endl;

shared_ptr

shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。


shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。


成员函数:


use_count 返回引用计数的个数


unique 返回是否是独占所有权( use_count 为 1)


swap 交换两个 shared_ptr 对象(即交换所拥有的对象)


reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少


get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptrsp(new int(1)); sp 与 sp.get()是等价的


weak_ptr

weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

class B;
class A
{
public:
shared_ptr<B> pb_;
~A()
{
     cout<<"A delete
";
}
};
class B
{
public:
shared_ptr<A> pa_;
~B()
{
    cout<<"B delete
";
}
};
void fun()
{
    shared_ptr<B> pb(new B());
    shared_ptr<A> pa(new A());
    pb->pa_ = pa;
    pa->pb_ = pb;
    cout<<pb.use_count()<<endl;
    cout<<pa.use_count()<<endl;
}
int main()
{
    fun();
    return 0;
}

可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用),如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr pb_; 改为weak_ptr pb_; 运行结果如下,这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。


注意:不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print(); 英文pb_是一个weak_ptr,应该先把它转化为shared_ptr,如:shared_ptr p = pa->pb_.lock(); p->print();


上一篇:数据结构之进制转换器实现


下一篇:干货收藏:68道C语言与C++常见面试题(三)