文章目录

  • 前言
  • 1、变量的声明和定义有什么区别?
  • 2、写出 bool int float 指针变量与零值比较的if语句
  • 3、sizeof和strlen的区别
  • 4、C语言的关键字static和c++的关键字static有什么区别
  • 5、c中的malloc和c++中的new有什么区别
  • 6、写一个标准的宏MIN
  • 7、一个指针可以使volatile吗
  • 8、a和&a有什么区别
  • 9、简述C,C++程序编译的内存分配情况
  • 10、简述strcpy,sprintf与memecpy的区别
  • 11、设置地址为ox67a9的整形变量的值为oxaa66
  • 12、面向对象的三大特征
  • 13、C++的空类有哪些成员函数
  • 14、谈谈你对拷贝构造函数和赋值运算符的认识
  • 15、简述类成员函数的重写,重载和隐藏的区别
  • 16、简述多态实现的原理
  • 17、链表和数组有什么区别
  • 18、怎么把一个单链表反序
  • 19、简述队列和栈的异同
  • 20、用C++设计一个不能被继承的类
  • 21、C++的引用和C语言的指针有什么区别
  • 22、typedef和define有什么区别
  • 23、常引用有什么作用
  • 24、构造函数能否为虚函数
  • 25、谈谈你对面向对象的认识

前言

在很多面试中,类C语言的面试中,基础问答是必不可少的,有时候恰恰就是最基础的问题难倒一片人,复杂的算法,反而游刃有余,当然也不是个个如此,所以本文将整理一些经典不过时的类C语言面试基础问题,记录如下,希望能给有帮助的人,避免到处百度,浪费不必要的时间

1、变量的声明和定义有什么区别?

常量:在程序执行过程中,不会发生改变的量,不能被改变的量

变量:在程序执行过程中,可以被改变的量

定义变量的方式:数据类型 变量名 = 常量;

int num = 10;  //定义(分配存储空间,初始化值为常量) 
int num; //定义性声明defining declaration(分配存储空间,分配垃圾值)
extern int num;//引用性声明referncing declaration,
//(不分配存储空间,不分配垃圾值)不限于本文件使用。
  • 变量的定义用于为变量分配存储空间,还可以为变量指定初始值。在一个程序中,变量有且仅有一个定义
  • 一般的情况下我们常常这样叙述,把建立空间的声明称之为“定义”,而把不需要建立存储空间的声明称之为“声明”。很明显我们在这里指的声明是范围比较窄的,即狭义上的声明,也就是说非定义性质的声明
  • 函数声明时,编译器不给函数分配入口地址
  • 函数定义时,函数代码段会被放到代码块中,这时候就算给函数分配存储空间了,函数名映射函数的入口地址。
  • 声明的最终目的是为了提前使用,即在定义之前使用,如果不需要提前使用就没有单独声明的必要,变量是如此,函数也是如此,所以声明不会分配存储空间,只有定义时才会分配存储空间

2、写出 bool int float 指针变量与零值比较的if语句

//bool型数据
If(flag)
If(!flag)

//int型数据:
if(0!=flag)
If(0==flag)

//指针型数据:
if(NULL==flag)
If(NUJLL!=flag)

//float 型数据:
Define NORM 0.00001;
if(flag>=-NORM && flag<=NORM)

注意:应特别注意在int,指针型变量和零值比较的时候,把零值放在左边,这样当把==误写成=时,编译器可以报错,否则这样逻辑错误不容易发现,并且可能导致很严重的后果。

3、sizeof和strlen的区别

sizeof和strlen 有以下区别:

sizeof是一个操作符,strlen是库函数

sizeof的参数可以使数据的类型,也可以是变量,而strlen只能是以‘\0’为结尾的字符串作参数。

编译器编译是就计算出了sizeof的结果,而strlen函数必须在运行是才能计算出来。并且sizeof计算的是数据类型占内存的大小,而strlen计算的是字符串实际的长度

数组做sizeof的参数不退化,传递strlen就退化成为指针了。

注意: 有些操作符看起来像是函数,而有些函数名看起来又像操作符,这类容易混淆的名称一定要加以区分,否则遇到数组名这类特殊数据类型做参数时就很容易出错。最容易混淆为函数的操作符就是sizeof。

4、C语言的关键字static和c++的关键字static有什么区别

在c中static用来修饰局部静态变量和外部静态变量,函数。而c++中除了上述功能外,还用来定义类的成员变量和函数,即静态成员变量和静态成员函数

注意: 编程时static的记忆性,和全局性的特点可以让在不同时期调用的函数进行通信,传递信息,而c++的静态成员则可以在多个对象实例间进行通信,传递信息。

5、c中的malloc和c++中的new有什么区别

Malloc和new有以下不同:

  1. new,delete是操作符,可以重载,只能在c++中使用
  2. Malloc,free是函数,可以覆盖,c,c++中都可以使用
  3. New可以调用对象的构造函数,对应的delete调用相应的析构函数
  4. Malloc仅仅分配内存,free仅仅回收内存,并不执行构造函数和析构函数
  5. New delete返回的是某种数据类型指针,malloc free返回的是void指针

注意:malloc申请的内存空间要用free释放,而new申请的内存空间要用delete释放,不要混淆,因为两者实现的机理不同

6、写一个标准的宏MIN

#define min(a,b)((a)<=(b)?(a):(b))

注意:在调用时一定要注意这个宏定义的副作用,如下调用

(++*p)<(x)?(++*p):(x)

P指针就自加了两次,违背了MIN的本意。

7、一个指针可以使volatile吗

volatile用在如下的几个地方:

1、中断服务程序中修改的供其它程序检测的变量需要加volatile;

2、多任务环境下各任务间共享的标志应该加volatile;

3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;

volatile 修饰指针一般用在 共享指针 上面。

下面代码:

uchar * volatile reg ;

上行代码里 volatile 修饰的是reg这个变量。所以这里实际上是定义了一个 uchar 类型的指针,并且这个指针变量本身是 volatile 的。但是指针所指的内容并不是volatile的!在实际使用的时候,编译器对代码中指针变量reg本身的操作不会进行优化,但是对reg所指的内容reg却会作为non-volatile内容处理,对reg的操作还是会被优化。通常这种写法一般用在对共享指针的声明 上,即这个指针变量有可能会被中断等函数修改。将其定义为volatile以后,编译器每次取指针变量的值的时候都会从内存中载入,这样即使这个变量已经被别的程序修改了当前函数用的时候也能得到修改后的值(否则通常只在函数开始取一次放在寄存器里,以后就一直使用寄存器内的副本)。

需要注意将上述代码与下面的代码进行区别:

volatile uchar * reg ;

这行代码里volatile修饰的是指针所指的内容。所以这里定义了一个uchar类型的指针,并且这个指针指向的是一个 volatile 的对象。 但是指针变量本身并不是 volatile 的。如果对指针变量reg本身进行计算或者赋值等操作,是可能会被编译器优化的。但是对reg所指向的内容reg的引用却禁止编译

器优化。因为这个指针所指的是一个volatile的对象 ,所以编译器必须保证对reg的操作都不被优化。通常在驱动程序的开发中,对 硬件寄存器指针 的定义,都应该采用这种形式。

而下面定义:

volatile uchar * volatile reg ;

这样定义出来的指针就本身是个volatile的变量,又指向了volatile的数据内容。

8、a和&a有什么区别

请写出以下代码的打印结果,主要目的是考察a和&a的区别

#include<stdio.h>
void main( void)
{
inta[5]={1,2,3,4,5};
int*ptr=(int *)(&a+1); //此处&a是数组的指针,1实际1个数组长度 1*5*4 = 20个字节;

printf("%d,%d\n",*(a+1),*(ptr-1));
}

输出结果:2,5

注意: 数组名a可以作数组的首地址,而&a是数组的指针。

&a+1,&a 为代表a数组的指针,1根据&a变化代表了int a[5]数组的大小,为45=20个字节。 a+1, a代表数组a的首地址,1代表数组a中1个元素的长度,为4个字节。 int ptr = (int )(&a + 1),代表ptr指向a数组20个字节后的一块内存区域,显然,该内存区域未定义,为一个随机值。而(ptr-1),此处ptr减去14个字节长度,ptr-1指向a[5],(ptr-1)=a[5];

思考:

将原式的int* ptr=(int *)(&a+1);

改为 int *ptr2=(int *)(a+1) 时输出的结果将是什么呢?

int ptr2 = (int )(a+1),代表指向数组首地址14个字节后元素,即a[1] = 2;即ptr2指向a[2],此处的a = *(a+0) = a[0], *(a+1) = a[1],依次类推。

9、简述C,C++程序编译的内存分配情况

(1)从静态存储区域分配:

内存在程序编译时就已经分配好,这块内存在程序的整个运行期间都存在,速度快,不容出错,因为有系统会善后,例如,全局变量,static变量等

(2)在栈上分配:

在执行函数时,函数内局部变量的存储单元都在栈上创建,函数在执行结束时这些存储单元自动被释放,栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

(3)从堆上分配:

即动态内存分配,程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由程序员决定,使用非常灵活,如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄露,另外频繁地分配和释放不同大小的堆空间将会产生堆内碎片。

一个C,C++程序编译时内存分为5大存储区:堆区,栈区,全局区,文字常量区,程序代码区

10、简述strcpy,sprintf与memecpy的区别

三者主要有以下不同之处

(1) 操作对象不同:strcpy的两个操作对象均为字符串,sprintf的操作对象可以是多种数据类型,目的操作对象是字符串,memcpy的两个对象是两个任意可操作的内存地址,并不限于何种数据类型。

(2) 执行功能不同:strcpy主要实现字符串变量间的拷贝,sprintf主要实现其他数据类型格式到字符串类型的转化,memcpy主要是内存块间的拷贝

(3) 执行效率不同:memcpy最高,sprintf最低

说明:strcpy,sprintf,memcpy都可以实现拷贝的功能,但是针对的对象不同,根据实际需求,来选择合适的函数实现拷贝功能

11、设置地址为ox67a9的整形变量的值为oxaa66

int *ptr;
Ptr=(int *)ox67a9;
*Ptr=oxaa66;

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

12、面向对象的三大特征

面向对象的三大特征是封装性,继承性,多态性。

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

(1)缺省的构造函数

(2)缺省的拷贝构造函数

(3)缺省的析构函数

(4)缺省的赋值运算符

(5)缺省取址运算符

(6)缺省取址运算符const

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

14、谈谈你对拷贝构造函数和赋值运算符的认识

拷贝构造函数和复制运算符重载有以下两个不同之处:

(1)拷贝构造函数生成新的类对象,而赋值运算不能

(2)由于拷贝 构造函数是直接构造一个新的类对象,所以在初始化这个对象之前不用检验源对象是否和新建对象相同,而赋值运算符则需要这个操作,另外赋值运算符中如果原来的对象中内存非配要先把内存释放掉

注意:当类中 有指针类型的成员变量时,一定要重写拷贝构造函数和赋值运算符,不要使用默认的。

15、简述类成员函数的重写,重载和隐藏的区别

(1)重写和重载主要有以下几点不同。

范围的区别:被重写的和重写的函数在两个类中,而重载和被重载的函数在同一个类中。

参数的区别:被重写函数和重写函数的参数列表一定相同,而被重载函数和重载函数的参数列表一定不同。

virtual的区别:重写的基类中被重写的函数必须要有virtual修饰,而重载函数和被重载函数可以被virtual修饰,也可以没有。

(2)隐藏和重写、重载有以下几点不同。

与重载的范围不同:和重写一样,隐藏函数和被隐藏函数不在同一个类中。

参数的区别:隐藏函数和被隐藏的函数的参数列表可以相同,也可不同,但是函数名肯定要相同。当参数不相同时,无论基类中的参数是否被virtual修饰,基类的函数都是被隐藏,而不是被重写。

说明:虽然重载和覆盖都是实现多态的基础,但是两者实现的技术完全不相同,达到的目的也是完全不同的,覆盖是动态态绑定的多态,而重载是静态绑定的多态。

16、简述多态实现的原理

编译器发现一个类中有虚函数,便会立即为此类生成虚函数表vtable,虚函数表的各表项为指向对应虚函数的指针,编译器还会在此类中隐含插入一个指针vptr(对vc编译器来说,它插在类中的第一个位置上)指向虚函数表,调用此类的构造函数时,编译器会隐含执行vptr与vtable的关联代码,将vptr指向对应的vtable,将类与此类的vtable联系起来,另外在调用类的构造函数时,指向基类的指针此时已经变成指向具体的类的this指针,这样依靠此this指针即可得到正确的vtable,如此才能真正与函数体进行连接,这就是动态联编,实现多态的基本原理。

注意:一定要区分虚函数,纯虚函数,虚拟继承的关系和区别。牢记虚函数实现原理,因为多态C++面试的重要考点之一,而虚函数是实现多态的基础。

17、链表和数组有什么区别

数组和链表有以下几点不同:

(1)存储形式:数组是一块连续的空间,声明时就要确定长度,链表是一块不连续的动态空间,长度可变,每个结点要保存相邻结点的指针。

(2)数据查找:数组的线性查找速度快,查找操作直接使用偏移地址,链表需要按顺序检索结点,效率低

(3)数据插入和删除:链表可以快速插入和删除结点,而数组则可能需要大量数据移动。

(4)越界问题:链表不存在越界问题,数组有越界问题

说明:在选择数组或链表数据结构时,一定要根据实际需要进行选择,数组便于查询,链表便于插入和删除,数组节省空间但长度固定,链表虽然变长但是占了更多的存储空间

18、怎么把一个单链表反序

一、反转单链表之循环算法

链表有两种: 带头结点的:头结点存储长度信息,头结点的next指向第一个实际节点; 不带头结点的,头结点即第一个节点; 这里使用带头结点的链表;

需要三个指针,记录当前节点(反转用)、前一个节点、后一个节点(反转之后前进用)。

代码如下:

/*
链表
1.带头结点的:head里面存放链表长度(或其他信息),head->next指向第一个实际节点;
2.不带头结点的:head即第一个实际节点
*/

typedef struct Node
{
int data;
Node * next;
};

void reverseList(Node *head)
{
if ((head==NULL)||((head->next)==NULL)) return ; //如果head为空,或者头结点指向空节点(链表长度为0),则退出。
Node *pre,*cur,*next; //cur 记录当前位置,pre记录上一个位置,为cur->next的值;next记录下一个位置,反转后cur不等于cur->next
cur=head->next;
pre=NULL; //逆转之后,头结点变为尾节点,其next为Null

while (cur!=NULL)
{
next=cur->next; //反转后不能再用cur->next,所以先记录下这个节点
cur->next=pre;
pre=cur;
cur=next;
}

head->next=pre; //cur已经为空,所以pre为尾节点。head->next指向它。
return ;
}

二、反转单链表之递归算法

代码如下:

List *reverse( List *oldList, List *newHead = NULL )
{
List *next = oldList-> next; //记录上次翻转后的链表
oldList-> next = newHead; //将当前结点插入到翻转后链表的开头
newHead = oldList; //递归处理剩余的链表
return ( next==NULL )? newHead: reverse( t, newHead );
}

19、简述队列和栈的异同

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

注意:区别栈区和堆区,堆区的存取是顺序随意,而栈区是后进先出,栈由编译器自动分配,存放函数的参数值,局部变量的值等,其操作方式类似数据结构中的栈,堆一般由程序员分配释放,若程序员不释放,程序结束时可能由os回收,分配方式类似于链表

它与本题的的堆和栈是两回事,堆栈只是一种数据结构,而堆区和栈区是程序中的不同内存存储区域。

20、用C++设计一个不能被继承的类

class A
{
private:
A() {}
~A() {}
};
class B : virtual public A
{
public:
B(){}
~B(){}
};
class C : virtual public B
{
public:
C() {}
~C() {}
};
void main( void )
{
B b;
//C c;
return;
}

注意:构造函数是继承实现的关键,每次子类对象构造时,首先调用的是父类的构造函数,然后才是自己的。

此处,如果将父类构造函数写成私有的,便不能完成继承操作。这是考察点。

21、C++的引用和C语言的指针有什么区别

指针和引用主要有以下区别:

(1)引用必须被初始化,但是不分配存储空间。指针不声明时初始化,在初始化的时候需要分配存储空间。//引用未初始化,编译器会报错![referencesmust be initialized]

(2)引用初始化以后不能被改变,指针可以改变所指的对象。

(3)不存在指向空值的引用,但是存在指向空值的指针。

注意:引用作为函数参数时,会引发一定的问题,因为让引用作参数,目的就是想改变这个引用所指向地址的内容,而函数调用时传入的是实参,看不出函数的参数是正常变量,还是引用,因此可能会引发错误。所以使用时一定要小心谨慎。

22、typedef和define有什么区别

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

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

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

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

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

23、常引用有什么作用

常引用的引入主要是为了避免使用变量的引用时,在不知情的情况下改变变量的值。常引用主要用于定义一个普通变量的只读属性的别名、作为函数的传入形参,避免实参在调用函数中被意外的改变。

说明:很多情况下,需要用常引用做形参,被引用对象等效于常对象,不能在函数中改变实参的值,这样的好处是有较高的易读性和较小的出错率。

void main(void)
{
int ival = 58;
void refFun(const int& xval);
refFun(ival);
cout << ival << endl; //不加const会返回59,加const会报错!
return;
}

void refFun(const int& xval)
{
xval = xval +1; //l-value specifies constobject[报错]
}

24、构造函数能否为虚函数

构造函数不能是虚函数。而且不能在构造函数中调用虚函数,因为那样实际执行的是父类的对应函数,因为自己还没有构造好。

析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。析构函数也可以是纯虚函数,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。

说明:虚函数的动态绑定特性是实现重载的关键技术,动态绑定根据实际的调用情况查询相应类的虚函数表,调用相应的虚函数。

class Base
{
public:
// virtual Base(){} //会报错:'inline' isthe only legal storage class for constructors
virtual ~Base(){}
};

int main()
{
Base objB;
cout << endl;
return 0;
}

25、谈谈你对面向对象的认识

面向对象可以理解成对待每一个问题,都是首先要确定这个问题由几个部分组成,而每一个部分其实就是一个对象。然后再分别设计这些对象,最后得到整个程序。传统的程序设计多是基于功能的思想来进行考虑和设计的,而面向对象的程序设计则是基于对象的角度来考虑问题。这样做能够使得程序更加的简洁清晰。

说明:编程中接触最多的“面向对象编程技术”仅仅是面向对象技术中的一个组成部分。发挥面向对象技术的优势是一个综合的技术问题,不仅需要面向对象的分析,设计和编程技术,而且需要借助必要的建模和开发工具。

个人认为:可以从项目开发的角度(实践)或面向对象的三大特性(继承、封装、多态)三点入手谈认识。