本章要讨论的这些问题都是C/C++设计语言中的难点,并且在各个企业面试中反复出现的考点。尤其是static和sizeof,它们在许多笔试以及面式的题目中都出出过,因此本章将详细讨论这些问题。
面试题13 使用sizeof计算普通变量所占空间大小
【解析】
代码第4行,str变量表示数组,对数组变量做sizeof运行得到的是数组占用内存的总大小。在这里,str的总大小为strlen("Hello")+1,注意数组中要有一个元素保存字符串结束符,所以sizeof(str)为6。
代码第5行和第6行的p和n分别是指针和int型变量。在32位WinNT平台下,指针和int都是4个字节,所以结果都是4.
代码第9行中的str是函数的参数,它在做sizeof运行时被认为是指针。这是因为当我们调用函数Func(str)时,由于数组是“传址”的,程序会在栈上分配一个4字节的指针来指向数组,因此结果也是4。
代码第11行中的p首先指向一个100字节的堆内存。 里还是对指针做sizeof()运行符,结果仍然是4。
总之,数组和指针的sizeof运算有细微的区别。如果数组变量传入函数中做sizeof运算,则和指针的运算没有区别,否则得到整个数组占用内存的大小。对于指针,无论是何种类型的指针,其大小都是固定的,在32位WinNT平台下都是4。
#include <stdio.h>
#include <stdlib.h>
void Func(char str[100])
{
printf("Func()=====sizeof(str) = %d\n", sizeof(str));
}
int main()
{
char str[] = "Hello";
char *p = str;
int n = 10;
printf("sizeof(str) ===== %d\n", sizeof(str));
printf("sizeof(p) ======= %d\n", sizeof(p));
printf("sizeof(n) ======= %d\n", sizeof(n));
void *ptr = malloc(100);
printf("sizeof(ptr) ===== %d\n", sizeof(ptr));
Func(str);
return 0;
}
面试题14 使用sizeof计算类对象所占空间大小
#include <iostream>
class A
{
public:
int i;
};
class B
{
public:
char ch;
};
class C
{
public:
int i;
short j;
};
class D
{
public:
int i;
short j;
char ch;
};
class E
{
public:
int i;
int ii;
short j;
char ch;
char chr;
};
class F
{
public:
int i;
int ii;
int iii;
short j;
char ch;
char chr;
};
int main()
{
std::cout << "sizeof(int) ======" << sizeof(int) << std::endl;
std::cout << "sizeof(short) ====" << sizeof(short) << std::endl;
std::cout << "sizeof(char) =====" << sizeof(char) << std::endl;
std::cout << std::endl;
std::cout << "sizeof(A) ========" << sizeof(A) << std::endl;
std::cout << "sizeof(B) ========" << sizeof(B) << std::endl;
std::cout << "sizeof(C) ========" << sizeof(C) << std::endl;
std::cout << "sizeof(D) ========" << sizeof(D) << std::endl;
std::cout << "sizeof(E) ========" << sizeof(E) << std::endl;
std::cout << "sizeof(F) ========" << sizeof(F) << std::endl;
return 0;
}
注意字节对齐
面试题15 使用sizeof计算含有虚函数的类对象的空间大小
#include <iostream>
using namespace std;
class Base
{
public:
Base(int x): a(x)
{
}
void print()
{
cout << "print ====== base===a=" << a << endl;
}
private:
int a;
};
class Derived: public Base
{
public:
Derived(int x):Base(x - 1), b(x)
{
}
void print()
{
cout << "print ====== derived===b=" << b << endl;
}
private:
int b;
};
面试题16 使用sizeof计算虚拟继承的类对象的空间大小
#include <iostream>
using namespace std;
class A
{
};
class B
{
};
class C: public A, public B
{
};
class D: virtual public A
{
};
class E: virtual public A, virtual public B
{
};
class F
{
public:
int a;
static int b;
};
int F::b = 10;
int main()
{
cout << "sizeof(A)=======" << sizeof(A) << endl;
cout << "sizeof(B)=======" << sizeof(B) << endl;
cout << "sizeof(C)=======" << sizeof(C) << endl;
cout << "sizeof(D)=======" << sizeof(D) << endl;
cout << "sizeof(E)=======" << sizeof(E) << endl;
cout << "sizeof(F)=======" << sizeof(F) << endl;
int a = 10;
cout << "sizeof(a)=======" << sizeof a << endl;
return 0;
}
运行结果:
面试题17 sizeof与strlen有哪些区别
【解析】
它们的区别如下:
sizeof是操作符,strlen是函数。
sizeof操作符的结果类型是size_t,它在头文件中typedef为unsigned int类型,该类型保证能容纳实现所建立的最大对象的字节大小。
sizeof可以用类型做参数,strlen只能用char*做参数,且必须是以'\0'结尾的。
数组做sizeof的参数不退化,传递给strlen就退化为指针了。
大部分编译程序在编译的时候sizeof就被计算过了,这就是sizeof(x)可以用来定义数组维数的原因,strlen的结果要在运行的时候才能计算出来,它用来计算字符串的长度,
不是类型占内存的大小。
sizeof后如果是类型,必须加括弧,如果是变量名,可以不加括弧。这是因为sizeof是个操作符,而不是个函数。
在计算字符串数组的长度上有区别。例如:
char str[20] = "0123456789";
int a = strlen(str);
int b = sizeof(str);
a计算的是以0x00结束的字符串的长度(不包括0x00结束符),这里结果是10.
b计算的则是分配的数组str[20]所占的内存空间的大小,不受里面存储内容的改变而改变。
如果要计算指针指向的字符串的长度,则一定要使用strlen,例如:
char *ss = "0123456789";
int a = strlen(ss);
int b = sizeof(ss);
a计算的是ss指针占用的内存空间大小,这里结果是4。
b计算的是ss指向的字符串的长度,这里结果是10。
面试题18 sizeof有哪些用途
【解析】
sizeof有以下用途。
与存储分配和I/O系统那样的例程进行通信。例如,
void* malloc(size_t size);
size_t fread(void* ptr, size_t sie, size_t nmemb, FILE* stream);
查看某个类型的对象在内存中所占的单元字节。例如:
void* memset(void* s, int c, sizeof(s));
在动态分配一对象时,可以让系统知道要分配多少内存。
便于一些类型的扩充,在Windows中很多结构类型就有一个专用的字段是用来放该类型的字节大小的。
由于操作数的字节数在实现时可能出现变化,建议在涉及操作数字节大小时用sizeof来代替常量计算。
如果操作数是函数中的数组形参或函数类型的形参,则sizeof给出其指针的大小。
面试题19 找错——使用strlen()函数代替sizeof计算字符串长度
【解析】
这个程序存在下面两方面的问题。
(1)函数UpperCase(char str[])的意图是将str指向的字符串小写字母换为大写字母。于是在代码第9行利用sizeof(str)/sizeof(str[0])获得数组中
的元素个数以便做循环操作。然而,sizeof(str)得到的并不是数组占用内存的总大小,而是一个字符指针的大小,为4字节。因此这里只能循环4次,
在代码第18行main()函数的调用中只能改变对数组的前4个字符进行转换。转换的结果为“ABCDe”。
(2)在代码第17行,这里的意图是使用要打印字符串的长度。然而,sizeof(str)/sizeof(str[0])计算的是数组元素的个数,比字符串的长度大1,
原因是数组的长度还包括字符串的结束符'\0'。
应该用strlen()函数来代替sizeof计算字符串长度。正确的代码如下:
#include <iostream>
#include <string>
void UpperCase(char str[])
{
int test = sizeof(str);
int test2 = sizeof(str[0]);
for(size_t i = 0; i < strlen(str); ++i)//计算字符串的长度
if('a' <= str[i] && str[i] <= 'z')
str[i] -= ('a' - 'A');
}
int main()
{
char str[] = "aBcDe";
std::cout << "The length of str is " << strlen(str) << std::endl;//计算字符串的长度
UpperCase(str);
std::cout << str << std::endl;
return 0;
}
面试题20 使用sizeof计算联合体的大小
#include <iostream>
union u
{
double a;
int b;
};
union u2
{
char a[13];
int b;
};
union u3
{
char a[13];
char b;
};
int main()
{
std::cout << "sizeof(u)====" << sizeof(u) << std::endl;
std::cout << "sizeof(u2)===" << sizeof(u2) << std::endl;
std::cout << "sizeof(u3)===" << sizeof(u3) << std::endl;
return 0;
}
面试题21 #pragmapack的作用
选择代码输出的正确结果。
#include <iostream>
#pragma pack(1)
struct test{
char c;
short s1;
short s2;
int i;
};
int main()
{
std::cout << "sizeof(test)====" << sizeof(test) << std::endl;
return 0;
}
A,9 B,10 C, 12 D,16
【解析】
代码第3行用#pragma pack将对齐设为1。由于结构体test的成员s1, s2和i的自身对齐分别为2, 2和4,都小于1。因此它们都是用1作为对齐,sizeof(test)=1+2+2+4=9.
如果注释代码第3行,则编译器默认对齐为8。所以叶个成员自身的对齐都小于8,因此它们使用自身的对齐,sizeof(test)=1+1(补齐)+2+2+2(补齐)+4=12
面试题22 为什么要引入内联函数
【解析】
引入内联函数的主要目的是,用它替代C语言中表达式形的宏定义来解决程序中函数调用的效率问题。在C语言里可以使用如下的宏定义。
#define ExpressionName(Var1, Var2) (Var1+Var2)*(Var1-Var2)
这种宏定义在形式及使用上像一个函数,但它使用预处理器实现,没有了参数压栈、代码生成等一系列的操作,因此效率很高。这种宏定义在形式上类似于一个函数,但在使用它时,仅仅只是做预处理器符号表中的简单替换,因此它不能进行参数有效性的检测,也就不能享受C++编译器严格类型检查的好处。另外,它的返回值也不能被强制转换为可转换的合适类型,这样,它的使用就存在着一系列的隐患和局限性。
另外,在C++中引入了类及类的访问控制,这样,如果一个操作或者说一个表达式涉及类的保护成员或私有成员,你就不可能使用这种宏定义来实现(因为无法将this指针放在合适的位置)。
inline推出的目的,也正是为了取代这种表达式形式的定义。它消除了它的缺点,同时又很好地继承了它的优点。
面试题23 为什么inline能很好地取代表达式形式的预定义
【解析】
有如下几种原因:
inline定义的类的内联函数,函数的代码被放入符号表中,在使用时直接进行替换(像宏一样展开),没有了调用的开销,效率也很高。
类的内联函数也是一个真正的函数。编译器在调用一个内联函数时,首先会检查它的参数的类型,保证调用正确;然后进行一系列的相关检查,就像对待任何一个真正的函数一样。这样就消除了它的隐患和局限性。
inline可以作为某个类的成员函数,当然就可以在其中使用所在类的保护成员及私有成员。
面试题24 说明内联函数使用的场合
首先使用inline函数可以完全取代表达式形式的宏定义。
内联函数在C++类中应用最广的,应该是用来定义存取函数。我们定义的类中一般会把数据成员定义成私有的或者保护的,
这样,外界就不能直接读写我们类成员的数据了。对于私有或者保护成员的读定就必须使用成员接口函数来进行。
如果我们把这些读写成员函数定义成内联函数的话,将会获得比较好的效率。例如下面的代码:
#include <iostream>
class A
{
private:
int nTest;
public:
int readTest()
{
return nTest;
}
void setTest(int i);
};
inline void A::setTest(int i)
{
nTest = i;
std::cout << "nTest======" << nTest << std::endl;
}
int main()
{
A a;
a.setTest(59);
std::cout << "readTest===" << a.readTest() << std::endl;
return 0;
}
类A的成员函数readTest()和setTest()都是inline函数。readTest()函数的定义体被放在类声明之中,因而readTest()自动转换成
inline函数;setTest()函数的定义体在类声明之外,因此要加上inline关键字。
面试题25 为什么不把所有的函数都定义成内联函数
【解析】
内联是以代码膨胀(复制)为代价的,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。以下情况不宜使用内联。
1.如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
2.如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
3.类的构造函数和析构函数容易让人误解成使用内联更有效要当心构造函数和析构函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。
所以不要随便地将构造函数和析构函数的定义体放在类声明中。一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这说明了inline不应该出现在函数的声明中)。
面试题26 内联函数与宏有什么区别
【解析】
二者区别如下:
1.内联函数在编译时展开,宏在预编译时展开。
2.在编译的时候,内联函数可以直接被镶嵌到目标代码中,而宏只是一个简单的文本替换。
3.内联函数可以完成诸如类型检测、语句是否正确等编译功能,宏就不具有这样的功能。
4.宏不是函数,inline函数是函数。
5.宏在定义时要小心处理宏参数(一般情况是把参数用括号括起来),否则容易出现二义性。而内联函数定义时不会出现二义性。