论述题1. strlen("\0")=?sizeof("\0")=?
strlen("\0")=0,sizeof("\0")=2。
strlen执行的是一个计数器的工作,它从内存的某个位置(可以是字符串开头,中间某个位置,甚至是某个不确定的内存区域)开始扫描,直到碰到第一个字符串结束符'\0'为止,然后返回计数器值。
sizeof是C语言的关键字,它以字节的形式给出了其操作数的存储大小,操作数可以是一个表达式或括在括号内的类型名,操作数的存储大小由操作数的类型决定。
具体而言,strlen与sizeof的差别表现在以下几个方面:
1)sizeof是关键字,而strlen是函数。sizeof后如果是类型必须加括弧,如果是变量名可以不加括弧。
2)sizeof操作符的结果类型是size_t,它在头文件中typedef为unsigned int类型。该类型保证能够容纳实现所建立的最大对象的字节大小。
3)sizeof可以用类型作为参数,strlen只能用char*做参数,而且必须是以"\0"结尾的。sizeof还可以以函数作为参数,如int g(),则sizeofi[g())的值等于sizeof(int)的值,在32位计算机下,该值为4。
4)当数组名做sizeof的参数时不退化,传递给strlen就退化为指针了。以数组char a[10]为例,在32位机器下,sizeof(a)=1*10=10,而传递给strlen就不一样了。
5)大部分编译程序的sizeof都是在编译的时候计算的,所以可以通过sizeof(x)来定义数组维数。而strlen的计算则是在运行期计算的,用来计算字符串的实际长度,不是类型占内存的大小。例如,char str[20]="0123456789",字符数组str是编译期大小已经固定的数组,在32位机器下,为1*20=20,而其strlen大小则是在运行期确定的,所以其值为字符串的实际长度10。
6)当用于计算一个结构类型或变量的sizeof时,返回实际的大小,当用于计算一个静态变量或数组时,返回整个数组所占用的大小,而sizeof不能返回动态数组大小。
7)数组作为参数传给函数时传的是指针而不是数组,传递的是数组的首地址。例如:
fun(char[8])
fun(char[])
都等价于fun(char*)。在C++里参数传递数组永远都是传递指向数组首元素的指针,编译器不知道数组的大小,如果想在函数内知道数组的大小,需要这样做:进入函数后用memcpy复制出来,长度由另一个形参传进去。
fun(unsiged char*p1,int len)
{
unsigned char* buf=new unsigned char[len+1];
memcpy(buf,p1,len);
}
程序示例1:
#include<stdio.h>
#include<string.h>
int main()
{
char arr[10]="Hello";
printf("%d\n",strlen(arr));
printf("%d\n",sizeof(arr));
return 0;
}
程序输出结果:
5
10
sizeof返回定义的arr数组时,编译器为其分配的数组空间大小不关心里面存了多少数据。strlen只关心存储的数据内容,不关心空间的大小和类型。
程序示例2:
#include<stdio.h>
#include<string.h>
int main()
{
char* parr=new char[10];
printf("%d\n",strlen(parr));
printf("%d\n",sizeof(parr));
printf("%d\n",sizeof(*parr));
return 0;
}
程序输出结果:
14
4
1
在上例中,程序定义了一个字符指针parr,它指向一个分配了10个空间的字符数组,由于没有进行初始化,根据strlen的计算原理,所以不能够确定sterlen(parr)的值,因为无法确定字符串的终止位置,所以该值为一个随机值,本例中输出为14。在32位机器下,parr为一个指针,所以sizeof(parr)的值为4,parr为指向字符的指针,所以sizeof(*parr)的值为1。
2. 对于结构体而言,为什么sizeof返回的值一般大于期望值
struct是一种复合数据类型,其构成元素既可以是基本数据类型,如int、double、float、short、char等,也可以是复合数据类型,如数组、struct、union等数据单元。
一般而言,struct的sizeof是所有成员对齐后长度相加,而union的sizeof是取最大的成员长度。
在结构中,编译器为结构的每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。
字节对齐也称为字节填充,它是C++编译器的一种技术手段,主要是为了在空间与复杂度上达到平衡。简单地讲,是为了在可接受的空间浪费的前提下,尽可能地提高对相同运算过程的最少(快)处理。字节对齐的作用不仅是便于CPU的快速访问,使CPU的性能达到最佳,而且可以有效地节省存储空间。例如,32位的计算机的数据传输值是4字节,64位计算机数据传输是8字节,这样struct在默认的情况上,编译器会对struct的结构进行(32位机)4的倍数或(64位机)8的倍数的数据对齐。对于32位机来说,4字节对齐能够使CPU访问速度提高,比如说一个long类型的变量,如果跨越了4字节边界存储,那么CPU要读取两次,这样效率就低了,但需要注意的是,如果在32位机中使用1字节或者2字节对齐,不仅不会提高效率,反而会使变量访问速度降低。
在默认情况下,编译器为每一个变量或数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变默认的对界条件:
1)使用伪指令#pragma pack(n),C编译器将按照n个字节对齐。
2)使用伪指令#pragma pack(),取消自定义字节对齐方式。
3)另外,还有如下的一种方式:_attribute((aligned(n))),让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。_attribute_((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。
例如如下数据结构:
struct test
{
charx1;
short x2;
float x3;
char x4;
};
由于编译器默认情况下会对struct作边界对齐,结构的第一个成员x1,其偏移地址为0,占据了第1个字节,第二个成员x2为short类型,其起始地址必须2字节对齐,因此编译器在x2和x1之间填充了一个空字节。结构的第三个成员x3和第四个成员x4恰好落在其自然边界地址上,在它们前面不需要额外的填充字节。在test结构中,成员x3要求4字节对界,是该结构所有成员中要求的最大边界单元,因而test结构的自然对界条件为4字节,编译器在成员x4后面填充了3个空字节。整个结构所占据空间为12字节。
再例如如下数据结构:
struct s1
{
short d;
int a;
short b;
}a1;
在32位机器下,short型占两个字节,int型占4个字节,所以为了满足字节对齐,变量d除了自身所占用的两个字节外,还需要再填充两个字节,变量a占用4个字节,变量b除了自身占用的两个字节,还需要两个填充字节,所以最终s1的sizeof为12。
字节对齐的细节和编译器实现相关,但一般而言,满足以下3个准则:
1)结构体变量的首地址能够被其最宽基本类型成员的大小所整除。
2)结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍。如有需要,编译器会在成员之间加上填充字节。
3)结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。
需要注意的是,基本类型是指前面提到的像char、short、int、float、double这样的内置数据类型,这里所说的“数据宽度”就是指其sizeof的大小,在32位机器上,这些基本数据类型的sizeof大小分别为1、2、4、4、8。由于结构体的成员可以是复合类型,所以在寻找最宽基本类型成员时,应当包括复合类型成员的子成员,而不是把复合成员看成是一个整体。如果一个结构体中包含另外一个结构体成员,那么此时最宽基本类型成员不是该结构体成员,而是取基本类型的最宽值。但在确定复合类型成员的偏移位置时,则是将复合类型作为整体看待,即复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度,达到程序优化的目的。
3. 指针进行强制类型转换后与地址进行加法运算,结果是什么
假设在32位机器上,在对齐为4的情况下,sizeof(long)的结果为4字节,sizeof(char*)的结果为4字节,sizeof(short int)的结果与sizeofl[short)的结果都为2字节,sizeof(char)的结果为1字节,sizeof(int)的结果为4字节,由于32位机器上是4字节对齐,以如下结构体为例:
struct BBB
{
long num;
char *name;
short int data;
char ha;
short ba[5];
}*p;
当p=0x1000000;则p+0x200=?(Ulong)p+0x200=?(char*)p+0x200=?
其实,在32位机器下,sizeof(struct BBB)=sizeof(*p)=4+4+2+1+1/*补齐*/+2*5+2/*补齐*/=24字节,而p=0x1000000,那么p+0x200=0x1000000+0x200*24指针加法,加出来的是指针类型的字节长度的整倍数,就是p偏移sizeof(p)*0x200。
(Ulong)p+0x200=0x1000000+0x200经过Ulong后,已经不再是指针加法,而变成一个数值加法了。
(char*)p+0x200=0x1000000+0x200*sizeof(char)结果类型是char*。
4. 使用指针有哪些好处
指针与其他类型变量一样,不同之处在于一般的变量包含的是实际的真实数据,而指针包含的是一个指向内存中某个位置的地址。指针好处众多,一般而言,使用指针有以下几个方面的好处:
1)可以动态分配内存。
2)进行多个相似变量的一般访问。
3)为动态数据结构,尤其是树和链表,提供支持。
4)遍历数组,如解析字符串。
5)高效地按引用“复制”数组与结构,特别是作为函数参数的时候,可以按照引用传递函数参数,提高开发效率。
5. 引用还是指针
程序设计中的引用其实就是别名的意思,它用于定义一个变量来共享另一个变量的内存空间,变量是一个内存空间的名字,如果给内存空间起另外一个名字,那就能够共享这个内存了,进而提高程序的开发效率。指针指向另一个内存空间的变量,可以通过它来索引另一个内存空间的内容,而指针本身也有自己的内存空间。
引用与指针有着相同的地方,即指针指向一块内存,它的内容是所指内存的地址,引用是某块内存的别名。但是,两者并非完全相同,它们之间也存在着差别,具体表现在以下几个方面:
1)从本质上讲,指针是存放变量地址的一个变量,在逻辑上是独立的,它可以被改变,即其所指向的地址可以被改变,其指向的地址中所存放的数据也可以被改变。而引用则只是一个别名而已,它在逻辑上不是独立的,它的存在具有依附性,所以弓l用必须在一开始就被初始化,而且引用的对象在其整个生命周期中是不能被改变的,即自始至终只能依附于同一个变量,具有“从一而终”的特性。
2)作为参数传递时,两者不同。在C++语言中,指针与引用都可以用于函数的参数传递,但是指针传递参数和引用传递参数有着本质的不同。指针传递参数本质上是值传递的方式,它所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,即在栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值。而在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。虽然它们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针传递的参数,如果改变被调函数中的指针地址,它将影响不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量,那就得使用指向指针的指针,或者指针引用。
3)引用使用时不需要解引用(*),而指针需要解引用。
4)引用只能在定义时被初始化一次,之后不能被改变,即引用具有“从一而终”的特性。而指针却是可变的,指针的初始化不是指指针的定义,而是指针变量存储的数值是个无效的数值。例如,定义float a,该语句表示a会分配一个地址,但初始值是一个随机的值,同样,float *a也会为a分配一个地址,初始值也是随机的值,初始化可以将a=NULL,这样在以后的程序中可以增加if(a==NULL)来判断指针是否有效,否则不行,或者为指针分配或者指定空间,如float *a=new float或者float b; float *a=&b,都可以为指针指向一块内存以实现初始化。
5)引用不可以为空,而指针可以为空。引用必须与存储单元相对应,一个引用对应一个存储单元。
6)对引用进行sizeof操作得到的是所指向的变量(对象)的大小,而对指针进行sizeof操作得到的是指针本身(所指向的变量或对象的地址)的大小,typeid(T)==typeid(T&)恒为真,sizeof(T)==sizeof(T&)恒为真,但是当引用作为成员时,其占用空间与指针相同。
7)指针和引用的自增(++)运算意义不一样。
8)如果返回动态分配的对象或内存,必须使用指针,引用可能引起内存泄露。
由于引用与指针的区别,所以并非所有使用指针的地方都可以使用引用,也并非所有使用引用的地方都可以使用指针,两者的使用也有其特定的环境。以如下实例为例进行分析。
1)int *a; int * & p=a; int b=8; p=&b; ∥正确,指针变量的引用
void & a=3; ∥不正确,没有变量或对象的类型是void
int & ri=NULL; ∥不正确,有空指针,无空引用
2)int & ra=int; ∥不正确,不能用类型来初始化
int *p=new int; int & r==*p; ∥正确
3)引用不同于一般变量,下面的类型声明是非法的:
int &b[3]; ∥不能建立引用数组
int & *p; ∥不能建立指向引用的指针
int &&r; ∥不能建立引用的引用
4)当使用&运算符取一个引用的地址时,其值为所引用变量的地址。
通过上面的实例可以发现,引用与指针都有其特定的使用场景,所以该使用指针时就使用指针,该使用引用时就使用引用,切不可混淆。
6. 指针和数组是否表示同一概念
指针可以随时指向任意类型的内存块,而数组可以在静态存储区被创建。例如,全局数组可以在栈上被创建。从原理与定义上看,虽然指针与数组表示的是不同的概念,但指针却可以方便地访问数组或者模拟数组,两者存在着一种貌似等价的关系,但也存在着诸多不同之处,主要表现在以下两个方面:
1)修改内容不同。
例如,char a[]="hello",可以通过取下标的方式对其元素值进行修改。例如,a[0]='X'是正确的,而对于char *P="world",此时P指向常量字符串,所以p[0]='X'是不允许的,编译会报错。
2)所占字节数不同。
例如,char *P="world",P为指针,则sizeof(p)得到的是一个指针变量的字节数,而不是P所指的内存容量。C/C++语言没有办法知道指针所指的内存容量,除非在申请内存时标记出来。
char a[]="hello world";
char *p=a;
在32位机器上,sizeof(a)=12字节,而sizeof(p)=4字节。
但需要注意的是,当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
void Func(char a[100])
{
cout<<sizeof(a);
}
此时sizeof(a)=sizeof(int)=4,而不是sizeof(int)*100=400。
8. 指针与数字相加的结果是什么
为了更有说服力地解释本题结果,首先将题目程序完善,在VC++6.0的环境下编译运行,程序源代码如下:
#include<stdio.h>
int main()
{
unsigned char *p1;
unsigned long *p2;
p1=(unsigned char*)0x801000;
p2=(unsigned long*)0x810000;
printf("%x\n",p1+5);
printf("%x\n",p2+5);
return 0;
}
程序输出结果如下:
801005
810014
p1=(unsigned char*)0x801000,是给指针变量赋值,把十六进制0x801000放到字符指针变量中,即指针变量p1的值就是0x801000。
p2=(unsigned long*)0x810000,也是给指针变量赋值,同上。
输出结果p1+5的值是801005,因为指针变量指向的值字符加1表示指针向后移动1个字节,那么加5代表向后移动5个字节,所以输出801005。
p2+5的值是801016,因为指针变量指向的是长整型,加1表示指针向后移动4个字节,那么加5代表向后移动5×4=20个字节,所以输入810014(十六进制)。
需要注意的是,内存的基本单位是字节,它以字节为存储单位储存,每个字节是8个二进制位,即8个bit。
9. 野指针?空指针
野指针是指指向不可用内存的指针。任何指针变量在被创建时,不会自动成为NULL指针(空指针),其默认值是随机的,所以指针变量在创建的同时应当被初始化,或者将指针设置为NULL,或者让它指向合法的内存,而不应该放之不理,否则就会成为野指针。而同时由于指针被释放(free或delete)后,未能将其设置为NULL,也会导致该指针变为野指针。虽然free和delete把指针所指的内存给释放掉了,但它们并没有把指针本身释放掉,一般可以采用语句if(p!=NULL)进行防错处理,但是if语句却起不到防错作用,因为即使p不是NULL指针,它也不指向合法的内存块。第三种造成野指针的原因是指针操作超越了变量的作用范围。
程序示例如下:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
chal *p=(char*)malloc(100);
strcpy(p,"hello");
free(p);
if(p!=NULL)
printf("Not NULL\n);
return 0;
}
程序输出:
Not NULL
上例中,虽然对p执行了free操作,p所指的内存被释放掉了,但是p所指的地址仍然不变,在后续的判断p是否为NULL时,根本没有起到防错的作用,所以程序输出仍然为Not NULL。
空指针是一个特殊的指针,也是唯一一个对任何指针类型都合法的指针。指针变量具有空指针值,表示它当时处于闲置状态,没有指向有意义的内容。为了提高程序的可读性,标准库定义了一个与0等价的符号常量NULL,程序里可以写p=0或者p=NULL,两种写法都把p置为空指针值。C语言保证这个值不会是任何对象的地址。给指针值赋零则使它不再指向任何有意义的东西。
作为一种风格,很多程序员一般不愿意在程序中到处出现未加修饰的0,所以习惯定义预处理宏NULL(在<stdio.h>和其他几个头文件中)为空指针常数,通常是0或者((void*)0)。希望区别整数0和空指针0的程序员可以在需要空指针的地方使用NULL。
通用指针可以指向任何类型的变量。通用指针的类型用(void*)表示,因此也称为void指针。
程序代码如下:
#include<stdio.h>
int main()
{
int n=3, *p;
void *gp;
gp=&n;
p=(int *)gp;
printf("%d\n",*p);
return 0;
}
程序输出结果:
3
10. C/C++头文件中的ifndef/define/endif的作用有哪些
如果一个项目中存在两个C文件,而这两个C文件都include(包含)了同一个头文件,当编译时,这两个C文件要一同编译成一个可运行文件,可能会产生大量的声明冲突。而解决的办法是把头文件的内容都放在#ifndef和#endif中,一般格式如下:
#ifndef<标识>
#define<标识>
...
...
#endif
上述代码的作用是当“当标识没有由#define定义过时,则定义标识。<标识>在理论上来说可以是自由命名的,但每个头文件的这个“标识”都应该是唯一的。标识的命名规则一般是头文件名全大写,前后加下画线,并把文件名中的“.”也变成下画线,如stdio.h。
#ifndef_STDIO_H_
#define_STDIO_H_
#endif
在#ifndef中定义变量出现的问题(一般不定义在#ifndef中)如下所示:
#ifndef AAA
#define AAA
...
int i;
...
#endif
里面有一个变量定义,在VC中链接时就出现了i重复定义的错误,而在C语言中成功编译。
11. #include<filename.h>和#include"filename.h"有什么区别
对于#include<filename.h>,编译器先从标准库路径开始搜索filename.h,使得系统文件调用较快。而对于#include "filename.h",编译器先从用户的工作路径开始搜索filename.h,然后去寻找系统路径,使得自定义文件较快。
引申:头文件的作用有哪些?
头文件的作用主要表现为以下两个方面:
1)通过头文件来调用库功能。出于对源代码保密的考虑,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口是怎么实现的。编译器会从库中提取相应的代码。
2)头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,大大减轻程序员调试、改错的负担。
12. #define有哪些缺陷
由于宏定义在预处理阶段进行,主要做的是字符替换工作,所以它存在着一些固有的缺陷:
1)它无法进行类型检查。宏定义是在编译前进行字符的替换,因为还没编译,不能编译前就检查好类型是否匹配,而只能在编译时才知道,所以不具备类型检查功能。
2)由于优先级的不同,使用宏定义时,可能会存在副作用。例如,执行加法操作的宏定义运算#defne ADD(a,b)a+b在使用的过程中,对于表达式的运算就可能存在潜在的问题,而应该改为#define ADD(a,b)((a)+(b))。
3)无法单步调试。
4)会导致代码膨胀。由于宏定义是文本替换,需要对代码进行展开,相比较函数调用的方式,会存在较多的冗余代码。
5)在C++中,使用宏无法操作类的私有数据成员。
13. 如何使用define声明一个常数,用以表明1年中有多少秒(忽略闰年问题)
#define SECOND_PER_YEAR(60*60*24*365)UL
在以上定义中,需要注意以下3个方面的内容:
1)由于宏定义是预处理指令,而非语句,所以在进行宏定义时,不能以分号结束。
2)预处理只会执行简单的替换,不会计算表达式的值,所以需要注意括号的使用,直接写出是如何计算出一年中有多少秒而不是计算出实际的值。
例如:
#define N4+5
cout<<2*N;
如果预处理计算表达式的值,那么输出结果应该是2x(4+5),等于18,可是实际输出结果却是2x4+5,等于13。
3)考虑到可能存在数据溢出问题,更加规范化的写法是使用长整型,即UL类型,告诉编译器这个常数是长整型数。
14. 含参数的宏与函数有什么区别
含参数的宏有时完成的是函数实现的功能,但是并非所有的函数都可以被含参数的宏所替代。具体而言,含参数的宏与函数的特点如下:
1)函数调用时,首先求出实参表达式的值,然后带入形参。而使用带参的宏只是进行简单的字符替换。
2)函数调用是在程序运行时处理的,它需要分配临时的内存单元;而宏展开则是在编译时进行的,在展开时并不分配内存单元,也不进行值的传递处理,也没有“返回值”的概念。
3)对函数中的实参和形参都要定义类型,两者的类型要求一致,如果不一致,应进行类型转换;而宏不存在类型问题,宏名无类型,它的参数也无类型,只是一个符号代表,展开时带入指定的字符即可。宏定义时,字符串可以是任何类型的数据。
4)调用函数只可得到一个返回值,而用宏可以设法得到几个结果。
5)使用宏次数多时,宏展开后源程序会变得很长,因为每展开一次都使程序内容增长,而函数调用不使源程序变长。
6)宏替换不占用运行时间,而函数调用则占运行时间(分配单元、保留现场、值传递、返回)。
7)参数每次用于宏定义时,它们都将重新求值,由于多次求值,具有副作用的参数可能会产生不可预料的结果。而参数在函数被调用前只求值一次,在函数中多次使用参数并不会导致多种求值过程,参数的副作用并不会造成任何特殊的问题。
一般来说,用宏来代表简短的表达式比较合适。
15. 宏定义平方运算#define SQR(X)X*X是否正确执行平方运算的宏定义不正确,会造成错误。下面以如下程序代码为例进行分析。
#include<stdio.h>
#define SQR(X)X*X
int main()
{
int a=21;
int k=2;
int m=1;
int b=SQR(k+m);
int c=SQR(k+m)/SQR(k+m);
a/=SQR(k+m)/SQR(k+m);
print f("%d\n%d\n%d\n",a,b,c);
return 0;
}
程序输出结果:
3
5
7
执行SQR(k+m)时,题目的意思是希望执行(k+m)*(k+m)操作,但因为宏定义中未能规范表示,导致在执行b=SQR(k+m)时,错误地执行为k+m*k+m=5;在执行c=SQR(k+m)/SQR(k+m)时,错误地执行为k+m*k+m*k+m=7。
注意求a的时候,宏定义是在预处理的时候进行的,a/=SQR(k+m)/SQR(k+m),不能先执行a=a/SQR(k+m)/SQR(k+m),而应该先计算:右边的值(卢操作符结合方向:从右到左),然后再执行复制操作,此例中a=a/7=3。
程序示例如下:
#include<stdio.h>
#define N 3
#define Y(n)((N+1)*n)
int main()
{
int p=Y(5+1);
int z=2*(N+Y(5+1));
printf("%d\n",z);
return 0;
}
程序输出结果:
48
上例中,P的值为21,z的值为48。Y(5+1)=((N+1)*5+1)=21。需要清楚的是,预处理在编译之前执行文本的替换工作。
程序示例如下:
#include <stdio.h>
#define F(a,b) a*b
int main()
{
printf("%d\n",F(3+6,8-5));
return 0;
}
程序输出结果:
46
即F(3+6,8-5)=3+6*8-5=46。
例如,int i=10,j=10,k=3;k*=i+j,应该首先计算i+j的值为20,然后再计算k的值,所以k的值为60。
16. 不能使用大于、小于、if语句,如何定义一个宏来比较两个数a、b的大小
如果只是进行简单的比较,则返回比较结果即可,宏定义可以写为如下形式:
#define check(a,b)(((a)-(b))==fabs((a)-(b)))?"greater":"smaller"
但如果需要返回较大的值,则宏定义可以写为
#define MAX(a,b)(abs((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,从而MAX(a,b)等b的值。
#define MAX(a,b)(((a)-(b))&0x80000000)?(b):(a)及#define MAx(a,b)(((b)-(a)&(0x1<<31))>>31)也都只能对整数进行操作。所以将#define MAX(a,b)(abs((a)-(b))==((a)-(b))?(a):(b))中的abs()函数换成fabs()函数,fabs()所接受的参数及返回值都是double型的,这样无论它是接受整数还是接受float型的数据,都不会因精度问题而出现误差。
引申:写一个“标准”宏MIN,这个宏输入两个参数并返回较小的一个。
由于此时没有限定,所以可以使用如下宏定义:
#define MIN(A,B)((A)<=(B)?(A):(B))
17. 如何判断一个变量是有符号数还是无符号数
判断一个变量是无符号数还是有符号数有以下3种方法:
1)采用取反操作。
对于这个变量分两种情况进行分析,一种情况是它为某种类型的值,另一种情况是它为某种类型。对于值而言,如果这个数以及其求反后的值都大于0,则该数为无符号数,反之则为有符号数,因为数据在计算机中都是以二进制的0或1存储的,正数以0开头,负数以1开头,求反操作符会把所有的1改为0,所有的0改为1。如果是有符号数,那么取反之后,开头的0会被改为1,开头的1会被改为0,开头为1时即表示该数为负数,如果是无符号数则不会受此影响。对于类型而言,也同样适用。
对于为值的情况,可以采用如下宏定义的方式:
#define ISUNSIGNED(a)(a>=0&&~a>=0)
对于为类型的情况,可以采用如下宏定义的方式:
#define ISUNSIGNED(type)((type)0-1>0)
前者一般只适用于K&R C,不适用于ANSI C的情况。
程序示例代码如下:
#include<stdio.h>
#define ISUNSIGNED(a) (a>=0)&&(~a>=0)
#define ISUNSIGNED_TYPE(type)((type)0-1>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()
{
int a=100;
int b=-1;
if(a<0)
{
printf("有符号数");
}
else
{
if(b-a>0)
printf("无符号数\n");
else
printf("有符号数\n");
}
return 0;
}
程序输出为
有符号数
上例中,当把变量a的类型变为unsigned int时,程序的输出则变为
无符号数
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;
}
程序输出为
无符号数
18. #define TRACE(S)(printf("%s\n",#s),s)是什么意思
#进行宏字符串连接,在宏中把参数解释为字符串,不可以在语句中直接使用。在宏定义中printf("%s\n",#S)会被解释为printf("%s\n","S")。
程序示例如下:
#include<stdio.h>
#include<string.h>
#define TRACE(S)(prinff("%s\n",#S),S)
int main()
{
int a=5;
int b=TRACE(a);
const char *str="hello";
char des[50];
strcpy(des,TRACE(str));
printf("%s\n",des);
return 0;
}
程序输出结果:
a
str
hello
上例中,宏定义又是一个逗号表达式,所以复制到des里面的值为后面S,也就是str的值。所以最后输出的就是字符串“hello”了。
19. 不使用sizeof,如何求int占用的字节数
一般求解字节数,最常采用的方法是采用sizeof求解。例如,在32位机器下,int型变量占用的内存空间大小为4个字节,而本题要求不使用sizeof,所以只能从原理上对int型变量所占的空间进行求解。
一般可以使用如下的方式实现:
#include<stdio.h>
#define MySizeof(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
上例中,(char*)& Value返回Value的地址的第一个字节,(char*)(&Value+1)返回 Value的地址的下一个地址的第一个字节,所以它们之差为它所占的字节数。
如果不使用宏定义的方式,也可以使用如下方式求解,程序示例代码如下:
#include<iostream>
using namespace std;
template<class Any>
int LengthofArray(Any* p)
{
return int(p+1)-int(p);
}
int main()
{
int* i;
double* q;
char a[10];
printf("%d\n",LengthofArray(i));
printf("%d\n",LengthofArray(q));
printf("%d\n",LengthofArray(&a));
return 0;
}
程序的输出结果:
4
8
10
20. 如何使用宏求结构体的内存偏移地址
#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;
int b;
float c;
double d;
char e;
};
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));
return 0;
}
在32位机器上,char型占1个字节,int型占4个字节,float型占4个字节,double占8个字节,所以经过VC++编译运行后,程序的输出为如下:
0
4
8
16
24
上述方法避免了实例化一个type对象,而且求值在编译期进行,没有运行期负担,程序效率大大提高。
21. 如何用sizeof判断数组中有多少个元素
只需要用整个数组的sizeof去除以一个元素的sizeof即可求出数组中元素的个数,以数组名array为例,代码为#define Count(sizeofi(aHay)/sizeof(array[0]))或者#define Count(sizeof(array)/sizeof(数组的类型,如int、double等))。程序示例如下:
#include<stdio.h>
#define Count(sizeof(array)/sizeof(array[0]))
int main()
{
int array[]={1,2,3,4,5};
printf("%d\n",Count);
return 0;
}
程序输出结果:
5
之所以以上两种写法都可以,是因为在数组中sizeofi(array[0])的值本质上就是sizeof(数组的类型,如int、double等),所以两者等价。
22. 枚举和define有什么不同
两者只有很小的区别。在C语言中,枚举为整型,枚举常量为int型,因此它们都可以和其他整型类别混用而不会出错,而且枚举优点众多:能自动赋值;调试器在检验枚举变量时,可以显示符号值;服从数据块作用域规则。具体而言,两者的区别表现在以下几个方面:
1)枚举常量是实体中的一种,而宏定义不是实体。
2)枚举常量属于常量,但宏定义不是常量。
3)枚举常量具有类型,但宏没有类型,枚举变量具有与普通变量相同的性质,如作用域、值等,但是宏没有。
4)#define宏常量是在预编译阶段进行简单替换,枚举常量则是在编译的时候确定其值。
5)一般在编译器里,可以调试枚举常量,但是不能调试宏常量。
6)枚举可以一次定义大量相关的常量,而#defme宏一次只能定义一个。
23. typdef和define有什么区别
typedef与define都是替一个对象取一个别名,以此来增强程序的可读性,但是它们在使用和作用上也存在着以下几个方面的不同:
1)原理不同。#define是C语言中定义的语法,它是预处理指令,在预处理时进行简单而机械的字符串替换,不做正确性检查,不管含义是否正确照样带入,只有在编译已被展开的源程序时才会发现可能的错误并报错。
例如,#define PI 3.1415926,当程序中执行area=PI*r*r语句时,PI会被替换为3.1415926,于是该语句被替换为area=3.1415926*r*r。如果把#define语句中的数字9写成了g,预处理也照样带入,而不去检查其是否合理、合法。
typedef是关键字,它在编译时处理,所以typedef有类型检查的功能。它在自己的作用域内给一个已经存在的类型一个别名,但是不能在一个函数定义里面使用标识符typedef。例如,typedef int INTEGER,这以后就可用INTEGER来代替int作整型变量的类型说明了,如:
INTEGER a,b;
用typedef定义数组、指针、结构等类型将带来很大的方便,不仅使程序书写简单而且使意义更为明确,因而增强了可读性。例如:
typedef int a[10];
表示a是整型数组类型,数组长度为10。然后就可用a说明变量,如a s1,s2;完全等效于int s1[10],s2[10]。同理,typedef void(*p)(void)表示p是一种指向void型的指针类型。
2)功能不同。typedef用来定义类型的别名,这些类型不只包含内部类型(int、char等),还包括自定义类型(如struct),可以起到使类型易于记忆的功能。
例如:typedef int(*PF)(const char*,const char*);
定义一个指向函数的指针的数据类型PF,其中函数返回值为int,参数为const char*。typedef还有另外一个重要的用途,那就是定义机器无关的类型。例如,可以定义一个叫REAL的浮点类型,在目标机器上它可以获得最高的精度:typedef long double REAL,在不支持long double的机器上,该typedef看起来会是下面这样:typedef double REAL,在double都不支持的机器上,该typedef看起来会是这样:typedef float REAL。
#define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。
3)作用域不同。#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而typedef有自己的作用域。
程序示例如下:
void fun()
{
#define A int
}
void gun()
{
∥在这里也可以使用A,因为宏替换没有作用域,但如果上面用的是typedef,那这里就不能用
∥A,不过一般不在函数内使用typedef
}
4)对指针的操作不同。两者修饰指针类型时,作用不同。
#define INTPTR1 int*
typedef int* INTPTR2;
INTPTR1 p1,p2;
INTPTR2 p3,p4;
INTPTR1 p1,p2和INTPTR2 p3,p4这两句的效果截然不同的。INTPTR1 p1,p2进行字符串替换后变成int* p1,p2,要表达的意义是声明一个指针变量p1和一个整型变量p2。而INTPTR2 p3,p4,由于INTPTR2是具有含义的,告诉我们是一个指向整型数据的指针,那么p3和p4都为指针变量,这句相当于int* p1,*p2。从这里可以看出,进行宏替换是不含任何意义的替换,仅仅为字符串替换;而用typedef为一种数据类型起的别名是带有一定含义的。
程序示例如下:
#define INTPTR1 int*
typedef int* INTPTR2;
int a=1;
int b=2;
int c=3;
const INTPTR1 p1=&a;
const INTPTR2 p2=&b;
INTPTR2 const p3=&c;
上述代码中,const INTPTR1 p1表示p1是一个常量指针,即不可以通过p1去修改p1指向的内容,但是p1可以指向其他内容。而对于const INTPTR2 p2,由于INTPTR2表示是一个指针类型,因此用const去限定,表示封锁了这个指针类型,因此p2是一个指针常量,不可使p2再指向其他的内容,但可以通过p2修改其当前指向的内容。INTPTR2 const p3同样声明的是一个指针常量。
24. C++中宏定义与内联函数有什么区别
宏代码本身不是函数,但使用起来却像函数,预处理器用复制宏代码的方式代替函数调用,省去了参数压栈、生成汇编语言的CALL调用、返回参数、执行return等过程,从而提高了速度。内联函数是代码被插入到调用者代码处的函数。对于C++而言,内联函数(inline)的作用也不是万能的,它的使用是有所限制的,它只适合函数体内代码简单的函数使用,不能包含复杂的结构控制语句(如while、switch),并且内联函数本身不能直接调用递归函数(自己内部还调用自己的函数)。
两者的区别主要表现在以下几个方面:第一,宏定义是在预处理阶段进行代码替换,而内联函数是在编译阶段插入代码;第二,宏定义没有类型检查,而内联函数有类型检查。
引申:内联函数与普通函数的区别有哪些?
内联函数的参数传递机制与普通函数相同,但是编译器会在每处调用内联函数的地方将内联函数的内容展开,这样既避免了函数调用的开销,又没有宏机制的缺陷。
内联函数和普通函数最大的区别在于其内部的实现方面上,普通函数在被调用时,系统首先要跳跃到该函数的入口地址,执行函数体,执行完成后,再返回到函数调用的地方,函数始终只有一个复制;而内联函数则不需要进行一个寻址的过程,当执行到内联函数时,此函数展开,如果在N处调用了此内联函数,则此函数就会有N个代码段的复制。
内联函数也并非是万金油,在使用的过程中也存在一定的局限性,如果函数体过大,编译器也会放弃内联方式,而采用普通的方式调用函数。此时,内联函数就和普通函数执行效率一样了。
25. 定义常量谁更好?#define还是const
尺有所短,寸有所长,define与const都能定义常量,效果虽然一样,但是各有侧重。define既可以替代常数值,又可以替代表达式,甚至是代码段,但是容易出错,而const的引入可以增强程序的可读性,它使程序的维护与调试变得更加方便。具体而言,它们的差异主要表现在以下几个方面:
1)define只是用来进行单纯的文本替换,define常量的生命周期止于编译期,不分配内存空间,它存在于程序的代码段,在实际程序中它只是一个常数,一个命令中的参数并没有实际的存在;而const常量存在于程序的数据段,并在堆栈中分配了空间,const常量在程序中确确实实地存在,并且可以被调用、传递。
2)const常量有数据类型,而define常量没有数据类型。
编译器可以对const常量进行类型安全检查,如类型、语句结构等,而define不行。
3)很多IDE支持调试const定义的常量,而不支持define定义的常量。
由于const修饰的变量可以排除程序之间的不安全性因素,保护程序中的常量不被修改,而且对数据类型也会进行相应的检查,极大地提高了程序的健壮性,所以一般更加倾向于用const来定义常量类型。
26. C语言中struct与union的区别是什么
struct(结构体)与union(联合体)是C语言中两种不同的数据结构,两者都是常见的复合结构,其区别主要表现在以下两个方面:
1)结构体与联合体虽然都是由多个不同的数据类型成员组成的,但不同之处在于联合体中所有成员共用一块地址空间,即联合体只存放了一个被选中的成员,而结构体中所有成员占用空间是累加的,其所有成员都存在,不同成员会存放在不同的地址。在计算一个结构型变量的总长度时,其内存空间大小等于所有成员长度之和(需要考虑字节对齐),而在联合体中,所有成员不能同时占用内存空间,它们不能同时存在,所以一个联合型变量的长度等于其最长的成员的长度。
2)对于联合体的不同成员赋值,将会对它的其他成员重写,原来成员的值就不存在了,而对结构体的不同成员赋值是互不影响的。
例如:typedef union{double i; int k[5]; char c;} DATE;
struct data {int cat; DATE cow; double dog;) too;
DATE max;则语句printf("%d",sizeof(struct date)+sizeof(max));的执行结果是多少?
假设为32位机器,n型占4个字节,double占8个字节,char型占1个字节,而DATE是一个联合型变量,联合型变量公用空间,union里面最大的变量类型是int[5],所以占用20个字节,它的大小是20,而由于union中dotlble占了8字节,因此union是要8字节对齐,所占内存空间为8的倍数。为了实现8字节对齐,所占空间为24。而data是一个结构体变量,每个变量分开占用空间,依次为sizeof(int)+sizeof(DATE)+sizeof(double)=4+24+8=36,按照8字节对齐,占用空间为40,所以结果为40+24=64。
27. 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是用户自定义数据类型(User Defined Type),它是没有权限设置的,它只能是一些变量的集合体,虽然可以封装数据却不可以隐藏数据,而且成员不可以是函数。为了和C语言兼容,C++中就引入了struct关键字。C++语言中的struct是抽象数据类型(ADT),它支持成员函数的定义,同时它增加了访问权限,它的成员函数默认访问权限为public。在用模板的时候只能写template<class Type>或template<typename Type>不能写template<struct Type>。
28. C++中struct与class的区别是什么
如果没有多态和虚拟继承,在C++中,struct和class的存取效率完全相同,存取class的数据成员与非虚函数效率和struct完全相同,不管该数据成员是定义在基类还是派生类的。
class的数据成员在内存中的布局不一定是数据成员的声明顺序,C++只保证处于同一个access section的数据成员按照声明顺序排列。
具体而言,在C++中,class和struct做类型定义时只有两点区别:首先是默认继承权限,class继承默认是private继承,而struct继承默认是public继承;其次是class还用于定义模板参数,就像typename,但关键字struct不用于定义模板参数。
C++中之所以保留struct关键字,主要有3个方面的原因:第一,保证与C语言的向下兼容性,C++必须提供一个struct;第二,C++中的struct定义必须百分之百地保证与C语言中的struct的向下兼容性,之所以把C++中最基本的对象单元规定为class而不是struct,就是为了能够避免各种兼容性要求的限制;第三,对struct定义的扩展使C语言代码能够更容易地被移植到C++中来。