[C/C++常见笔试面试题] 程序设计基础 - 预处理、结构体与类篇

5 预处理

预处理也称为预编译,它为编译做预备工作,主要进行代码文本的替换工作,用于处理#开头的指令,其中预处理器产生编译器的输出。下图所示为常见的一些预处理指令及其功能。

5.1 C/C++头文件中的ifndef/define/endif的作用有哪些?

如果一个项目中存在两个C文件,而这两个C文件都include (包含)了同一个头文件, 当编译时,这两个C文件要一同编译成一个可运行文件,可能会产生大量的声明冲突。而解决的办法是把头文件的内容都放在#ifhdef和#endif中,一般格式如下:

#ifndef <标识>

#define〈标识〉

#endif

上述代码的作用是当标识没有#define定义过时,则定义标识。 <标识> 在理论上来说可以是自由命名的,但每个头文件的这个“标识”都应该是唯一的。标识的命名规则一般是头文件名全大写,前后加下划线,并把文件名中的 “.” 也变成下划线,如stdio.h。

#ifndef _STDIO_H_
#define _STDIO_H_ 
...
#endif


5.2 #include <filename.h>和#include “filename.h” 有什么区別?

对于#include <filename.h>,编译器先从标准库路径开始搜索filename.h,然后从本地目录搜索,使得系统文件调用较快。而对于#include “filename.h”,编译器先从用户的工作路径开始搜索filename.h,后去寻找系统路径,使得自定义文件较快。

5.3 头文件的作用有哪些?

头文件的作用主要表现为以下两个方面:

(1) 通过头文件来调用库功能。出于对源代码保密的考虑,源代码不便(或不准)向用户 公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口是怎么实现的。编译器会从库中提取相应的代码。

(2) 头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,能大大减轻程序员调试、改错的负担。


5.4 #define有哪些缺陷?

由于宏定义在预处理阶段进行,主要做的是字符替换工作,所以它存在着一些固有的缺陷:

(1) 它无法进行类型检查。宏定义是在编译前进行字符的替换,因为还没编译,不能编译前就检查好类型是否匹配,而只能在编译时才知道,所以不具备类型检查功能。

(2) 由于优先级的不同,使用宏定义时,可能会存在副作用。例如,执行加法操作的宏定义运算#defme ADD(a,b) a+b在使用的过程中,对于表达式的运算就可能存在潜在的问题,而应该改#define ADD(a,b) ((a)+(b))。

(3) 无法单步调试。

(4) 会导致代码膨胀。由于宏定义是文本替换,需要对代码进行展开,相比较函数调用的 方式,会存在较多的冗余代码。

(5) 在C++中,使用宏无法操作类的私有数据成员。


5.5 如何使用define声明一个常数,用以表明1年中有多少秒(忽略闰年间题)?

#define SECOND_PER_YEAR (365*24*60*60)UL

考虑到可能存在数据溢出问题,更加规范化的写法是使用长整型类型,即UL类型, 告诉编译器这个常数是长整型数。


5.6 宏定义平方运算#define SQR(X) X*X是否正确?

执行平方运算的宏定义不正确,会造成错误。例如SQR(k+m),你以为会是(k+m)*(k+m),但实际上是k+m*k+m。再举一个例子,SQR(k+m)/SQR(k+m),会错误地执行为k+m*x+m/k+m*k+m


5.7 不能使用大于、小于、if语句,如何定义一个宏来比较两个数a、b的大小?

如果只是进行简单的比较,则返回比较结果即可,宏定义可以写为如下方式: #define check(a,b) (((a)-(b))=fabs((a)-(b)))? “greater”: “smaller”

#define check(a,b)   (((a)-(b))=fabs((a)-(b)))? “greater”: “smaller”   

但如果需要返回较大的值,则宏定义可以写为

#define MAX(a,b) (fabs((a)-(b))=((a)-(b))?(a):(b))

虽然#define MAX(a,b) (abs((a)-(b))=((a)-(b))?(a):(b))也是一种比较好的做法,但是函数 abs() 接收的参数及其返回值都是整数,这样在传递实参时,其小数部分可能被截去,从而导致误差。例如,a=-12.345, b=-24.1467, abs((a)-(b))返回值为 12,但(a)-(b)显然不等于 12。

fabs()所接受的参数及返回值都是double型的,这样无论它是接受整数还是接受float型的数据,都不会因精度问题而出现误差。


5.8 如何判断一个变量是有符号数还是无符号数?

判断一个变量是无符号数还是有符号数有以下3种方法:

(1) 采用取反操作。

对于这个变量分两种情况进行分析,一种情况是它为某种类型的值,另一种情况是它为某种类型。对于值而言,如果这个数以及其求反后的值都大于0,则该数为无符号数,反之则为有符号数,因为数据在计算机中都是以二进制的0或1存储的,正数以0开头,负数以1开头,求反操作符会把所有的1改为0,所有的0改为1。如果是有符号数,那么取反之后,开头的0会被改为1,开头的1会被改为0,开头为1时即表示该数为负数,如果是无符号数则不会受此影响。对于类型而言,也同样适用。

对于为值的情况,可以采用如下宏定义的方式:

#defme ISUNSIGNED(a) (a>=0 && ~a>=0)

对于为类型的情况:

#define ISUNSIGNED(type) ((type)O-l > 0)

程序示例代码如下:

#include <stdio.h>

#define ISUNSIGNED(a)   (a >= 0) && (〜a >= 0)
#define ISUNSIGNED_TYPE(type) ((type)O-l > 0) 

int main()
{
    int a = 0;
    unsigned int b = 0;
    
    printf("%d \n", ISUNSIGNED(a));
    printf("%d \n", ISUNSIGNED(b));
    printf("%d \n", ISUNSIGNED_TYPE(int));
    printf("%d \n", ISUNSIGNED_TYPE(unsigned int));
    
    return 0;
}

程序输出结果:

0
1
0
1

(2) 由于无符号数和有符号数相减的结果为无符号,所以还可以采用以下方法判断:

#include<stdio.h>

int main()
{
    unsigned int a = 100; 
    int b = -1;
    
    if(a<0)
    {
        printf("有符号数”);
    }
    else
    {
        if(b-a>0)
        printf("无符号数 \n");
        else
        printf^有符号数\nM);
    }
    
    return 0;
}

程序输入为:

无符号数

(3) 通过改变符号位判断。把A进行一个位运算,将最高位置1,判断是否大于0。 程序示例如下:

#include <stdio.h> 

int main()
{
    unsigned A = 10;

    A = A|(1 << 31); 
    if(A > 0)
        printf("无符号数 \n");
    else
        printf("有符号数 \n”); 
    
    return 0;
}    

程序输出为

无符号数


5.9 #define TRACE(S) (printf("%s\n",#S),S)是什么意思?

#进行宏字符串连接,在宏中把参数解释为字符串,不可以在语句中直接使用。在宏定义 中 printf("%s\n", #S)会被解释为 printf("%s\n”,”S”)。

程序示例如下:

#include <stdio.h>

#define TRACE(S) (printf("%s\n", #S),S) 

int main()
{
    int a = 5;
    TRACE(a);

    char *str = "hello";
    char des[20];
    strcpy(des,TRACE(str));
    printf("des=%s\n",des);
    
    return 0;
}

程序输出结果:

a
str
hello

上例中,宏定义又是一个逗号表达式,所以复制到des里面的值为后面S也就是str的值。所以最后输出的就是字符串hello了。


5.10 不使用sizeof,如何求int占用的字节数?

(1)使用宏定义的方式:

#include <stdio.h>

#define MY_SIZEOF(value) ( (char *)(&value+1) - (char *)&value )

int main()
{
    int i; 
    double f; 
    double a[4]; 
    double *q;
    
    printf("%d\n",MySizeof(i)); 
    printf("%d\n",MySizeof(f)); 
    printf("%d\n",MySizeof(a)); 
    printf("%d\n",MySizeof(q)); 
    
    return 0;
}

程序输出如下:

4
8
32
4

注意使用#define MY_SIZEOF(value) ( (&value+1) - &value ),返回的是1(偏移单位长度),而不是4。

(2)使用函数的方式:

#include <iostream>
using namespace std;

template <class T>
int getSize(T *p)
{
    return int(p+1)-int(p);
}

int main()
{
    int* i; 
    double* q; 
    char a[10];
    
    printf("%d\n",getSize(i)); 
    printf("%d\n",getSize(q)); 
    printf("%d\n",getSize(&a)); 
    
    return 0;
}

程序输出如下:

4
8
10

注意使用#return (p+1)-(p);,返回的是1(偏移单位长度),而不是4。


5.11 如何使用宏求结构体的内存偏移地址?

#define OFFSET(type,field) ( (size_t)&(((type *)0)->field) )

在C语言中,ANSI C标准允许值为0的常量被强制转换成任何一种类型的指针,而且转 换结果是一个空指针,即NULL指针,因此对0取指针的操作((type )0)的结果就是一个类型 为type*的NULL指针。但如果利用这个NULL指针来访问type的成员当然是非法的,因为 &((type )0)->field)的意图只不过是计算field字段的地址。

C语言编译器根本就不生成访问type的代码,而仅仅是根据type的内容布局和结构体实例首址在编译期计算这个(常量)地址,这样就完全避免了通过NULL指针访问内存可能出现的问题。同时又因为地址为0,所以这个地址的值就是字段相对于结构体基址的偏移。

程序示例如下:

#include <stdio.h>

#define OFFSET(type,field) ( (size_t)&(((type *)0)->field) )

struct MyStr
{
    char a; //4byte (由于下一个成员为4个字节的,为了字节对齐,补齐为4个字节)
    int b;  //8byte
    float c; //16byte (由于下一个成员为8个字节的,为了字节对齐,补齐为8个字节)
    double d; //24byte
    char e; //32byte (要是最大长度的整数倍)
};

int main()
{
    printf("%d\n",OffSet(MyStr,a)); 
    printf("%d\n",OffSet(MyStr,b)); 
    printf("%d\n",OffSet(MyStr,c)); 
    printf("%d\n",OffSet(MyStr,d)); 
    printf("%d\n",OffSet(MyStr,e)); 
    printf("%d\n",sizeof(MyStr));
    
    return 0;
}

程序输出如下:

0
4
8
16
24
32

上述方法避免了实例化一个type对象,而且求值在编译期进行,没有运行期负担,程序效率大大提高。


5.12 如何用sizeof判断数组中有多少个元素?

#define COUNT_ARR(arr) (sizeof(arr)/sizeof(arr[0]))


5.13 枚举和define有什么不同?

具体而言,两者的区别表现在以下几 个方面:

(1) 枚举常量是实体中的一种,而宏定义不是实体。

(2) 枚举常量属于常量,但宏定义不是常量。

(3) 枚举常量具有类型,但宏没有类型,枚举变量具有与普通变量相同的性质,如作用域、值等,但是宏没有。

(4) #define宏常量是在预编译阶段进行简单替换,枚举常量则是在编译的时候确定 其值。

(5) —般在编译器里,可以调试枚举常量,但是不能调试宏常量。

(6) 枚举可以一次定义大量相关的常量,而#define宏一次只能定义一个。


5.14 typdef和define有什么区别?

typedef与define都是替一个对象取一个别名,以此来增强程序的可读性,但是它们在使用和作用上也存在着以下几个方面的不同:

(1) 原理不同。#define是C语言中定义的语法,它是预处理指令,在预处理时进行简单而机械的字符串替换,不作正确性检查,不管含义是否正确照样带入,只有在编译己被展开的源程序时才会发现可能的错误并报错。typedef

是关键字,它在编译时处理,所以typedef有类型检查的功能。

(2) 功能不同。typedef用来定义类型的别名,这些类型不只包含内部类型(int、char 等),还包括自定义类型(如struct),可以起到使类型易于记忆的功能。例如定义一个函数指针。#define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。

(3) 作用域不同。#defme没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而typedef有自己的作用域。

程序示例如下:

void fun()
{
    #define A int                                           
}

void gun()
{
//在这里也可以使用A,因为宏替换没有作用域,但如果上面用的是typedef,那这里就不能用 
//不过一般不在函数内使用typedef
}

(4) 对指针的操作不同。两者修饰指针类型时,作用不同。

#defme INTPTR1 int*
typedef int* INTPTR2;

INTPTR1 pl,p2;
INTPTR2 p3,p4;

INTPTR1 p1,p2和INTPTR2 p3,p4这两句的效果截然不同的。INTPTR1 p1,p2进行字符串替换后变成int* p1,p2,要表达的意义是声明一个指针变量p1和一个整型变量p2。而 INTPTR2 p3,p4,由于INTPTR2是具有含义的,告诉我们是一个指向整型数据的指针,那么p3和p4都为指针变量。


5.15 C++中宏定义与内联函数有什么区别?

两者的区别主要表现在以下几个方面:第一,宏定义是在预处理阶段进行代码替换,而内联函数是在编译阶段插入代码;第二,宏定义没有类型检查,而内联函数有类型检查。


5.16 #define和const的区别是什么?

define与const都能定义常量,效果虽然一样,但是各有侧重。 define既可以替代常数值,又可以替代表达式,甚至是代码段,但是容易出错,而const的引入可以增强程序的可读性,它使程序的维护与调试变得更加方便。具体而言,它们的差异主要 表现在以下几个方面:

(1) define只是用来进行单纯的文本替换,define常量的生命周期止于编译期,不分配内存空间,它存在于程序的代码段,在实际程序中它只是一个常数,一个命令中的参数并没有实际的存在;而const常量存在于程序的数据段,并在堆栈中分配了空间,const常量在程序中确确实实地存在,并且可以被调用、传递。

(2) const常量有数据类型,而define常量没有数据类型。编译器可以对const常量进行类型安全检查,如类型、语句结构等,而define不行。

(3) 很多IDE支持调试const定义的常量,而不支持define定义的常量。


6 结构体与类

不要因为c和C++中有一些语法和关键字看上去相同,就认为它们的意义和作用完全一样。有些时候它们是不一样的,如struct (结构体)与class (类)。

6.1 C语言中struct和union的区别是什么?

struct (结构体)与union (联合体)是C语言中两种不同的数据结构,两者都是常见的复合结构,其区别主要表现在以下两个方面:

(1) 结构体与联合体虽然都是由多个不同的数据类型成员组成的,但不同之处在于联合体中所有成员共用一块地址空间,即联合体只存放了一个被选中的成员,而结构体中所有成员占用空间是累加的,其所有成员都存在,不同成员会存放在不同的地址。所以结构体在计算一个结构型变量的总长度时,其内存空间大小等于所有成员长度之和(需要考虑字节对齐),而在联合体中, 所有成员不能同时占用内存空间,它们不能同时存在,所以一个联合型变量的长度等于其最长的成员的长度。

(2) 对于联合体的不同成员赋值,将会对它的其他成员重写,原来成员的值就不存在了, 而对结构体的不同成员赋值是互不影响的。

示例程序如下:

typedef union {double i; int k[5]; chare;} DATE; 
struct data {int cat; DATE cow; double dog;} max;
printf("%d",sizeof(struct date)+sizeof(max));

假设为32位机器,DATE是一个联合型变量,联合型变量公用空间,里面最大的变量类型是int[5],所以占用20个字节,而由于union中double占了8个字节,为了实现8字节对齐,所以union所占空间为24。而data是一个结构体变量,每个变量分开占用空间,依次为sizeof(int) + sizeof(DATE)+sizeof(double)=4+24+8=36,而double为8字节,为了8字节对齐,占用空间为40。所以打印结果是 24 + 40 = 64 个字节。


6.2 C和C++中struct的区别是什么?

C语言中的struct与C++中的struct的区别表现在以下3个方面:

(1) C语言的struct不能有函数成员,而C++的struct可以有。

(2) C语言的struct中数据成员没有private、public和protected访问权限的设定,而C++ 的struct的成员有访问权限设定。

(3) C语言的struct是没有继承关系的,而C++的struct却有丰富的继承关系。

C语言中的struct是用户自定义数据类型,它是没有权限设置的,它只能是一些变量的集合体,虽然可以封装数据却不可以隐藏数据,而且成员不可以是函数。为 了和C语言兼容,C++中就引入了 struct关键字。C++语言中的struct是抽象数据类型 (ADT),它支持成员函数的定义,同时它增加了访问权限,它的成员函数默认访问权限为 public。


6.3 struct与class的区别是什么?

具体而言,在C++中,class和struct做类型定义时只有两点区别:

(1) 默认继承权限不同。class继承默认是private继承,而struct继承默认是public继承;

(2) class还用于定义模板参数,就像typename,但关键字struct不用于定义模板参数。

C++中之所以保留struct关键字,主要有3个方面的原因:第一,保证与C语言的向下兼容性,C++必须提供一个struct;第二,C++中的struct定义必须百分之百地保证与C语言中的 struct的向下兼容性,之所以把C++中最基本的对象单元规定为class而不是struct,就是为了能够避免各种兼容性要求的限制;第三,对struct定义的扩展使C语言代码能够更容易地被移植到C++中来。

猜你喜欢

转载自www.cnblogs.com/linuxAndMcu/p/10199453.html