C++程序员面试宝典——预处理、counst与sizeof

-------------------------------------------------基本原理----------------------------------------------------

C++程序设计三大难点:预处理、counst与sizeof

1. 预处理

C++从C语言那里把C语言预处理器继承过来(C语言预处理器,被Bjarne博士简称为Cpp)。

主要作用就是:    把通过预处理的内建功能对一个资源进行等价替换.

最常见的预处理有: 文件包含,条件编译、布局控制和宏替换4种。 

文件包含:    #include 是一种最为常见的预处理,主要是做为文件的引用组合源程序正文。 
条件编译:    #if,#ifndef,#ifdef,#endif,#undef等也是比较常见的预处理,主要是进行编译时进行有选择的挑选,注释掉一些指定的代码,以达到版本控制、防止对文件重复包含的功能。 
布局控制:    #progma,这也是我们应用预处理的一个重要方面,主要功能是为编译程序提供非常规的控制流信息。 
宏替换:    #define,这是最常见的用法,它可以定义符号常量、函数功能、重新命名、字符串的拼接等各种功能。

 (1)简单的宏定义

#define <宏名>  <字符串>

例: #define PI 3.1415926

(2) 带参数的宏定义

#define <宏名> (<参数表>) <宏体>

例: #define A(x) x

常见的预处理指令: 

     #define         宏定义 
     #undef          取消宏 
     #include        文本包含 
     #ifdef            如果宏被定义就进行编译 
     #ifndef          如果宏未被定义就进行编译 
     #endif           结束编译块的控制 
     #if                表达式非零就对代码进行编译 
     #else            作为其他预处理的剩余选项进行编译 
     #elif              这是一种#else和#if的组合选项 
     #line             改变当前的行数和文件名称 
     #error            输出一个错误信息 
     #pragma        为编译程序提供非常规的控制流信息 

2. const

Const 是C++中常用的类型修饰符,常类型是指使用类型修饰符const说明的类型,常类型的变量或对象的值是不能被更新的。

关于C++ const 的全面总结

Const主要有几点优势:

保护功能:便于进行类型检查;可以保护被修饰的东西;为函数重载提供了一个参考

时空效率:相比宏常量的反复分配内存空间,const常量只分配一次内存空间;编译器将它们保存在符号表中,成为编译常量,没有了存储与读取的操作。

指针使用CONST

(1)指针本身是常量不可变

     (char*) const pContent; 

     const (char*) pContent; 

(2)指针所指向的内容是常量不可变

     const (char) *pContent; 

     (char) const *pContent; 

(3)两者都不可变

      const char* const pContent; 

(4)还有其中区别方法,沿着*号划一条线

如果const位于*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;(const在左—>内容常量)

如果const位于*的右侧,const就是修饰指针本身,即指针本身是常量。(const在右—>指针常量)

函数中使用CONST

任何不修改数据成员的函数都应该被定义为const成员函数。

常量函数结尾使用const,表明该函数不会修改调用的实参。

像一般的输出函数display(),print(),show()等都可定义为常量函数。

如:int size()  const;

如果const放在函数开头,一般用来表示返回一个常量。

a.传递过来的参数在函数内不可以改变(无意义,因为Var本身就是形参)

void function(const int Var);

b.参数指针所指内容为常量不可变

void function(const char* Var);

c.参数指针本身为常量不可变(也无意义,因为char* Var也是形参)

void function(char* const Var);

d.参数为引用,为了增加效率同时防止修改。修饰引用参数时:

void function(const Class& Var); //引用参数在函数内不可以改变

将Const类型转化为非Const类型的方法

采用const_cast 进行转换。  

用法:const_cast <type_id>  (expression) 

类里面的成员变量加mutable修饰const成员函数,就可以修改它了。

对比学习内容:volatile

与const一样,volatile是一个类型修饰符。如果不加入volatile:要么无法编写多线程程序,要么编译器失去大量优化的机会。

volatile的作用是: 作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值

使用该关键字的例子如下:

volatile int vint;

当要求使用volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。volatile 指出 i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在b中。

而优化做法是,由于编译器发现两次从i读数据的代码之间的代码没有对i进行过操作,它会自动把上次读的数据放在b中。如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的稳定访问

假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是真正懂得volatile完全的重要性。

1). 一个参数既可以是const还可以是volatile吗?解释为什么。

2). 一个指针可以是volatile 吗?解释为什么。

3). 下面的函数被用来计算某个整数的平方,它能实现预期设计目标吗?如果不能,试回答存在什么问题:

int square(volatile int *ptr)
{
    return ((*ptr) * (*ptr));
}

下面是答案:

1). 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。

2). 是的。尽管这并不很常见。一个例子是当一个中断服务子程序修改一个指向一个buffer的指针时。

3). 这段代码是个恶作剧。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:

int square(volatile int* &ptr)//这里参数应该申明为引用,不然函数体里只会使用副本,外部没法更改
{
    int a,b;
    a = *ptr;
    b = *ptr;
    return a*b;
}

由于*ptr的值可能在两次取值语句之间发生改变,因此a和b可能是不同的。结果,这段代码可能返回的不是你所期望的平方值!正确的代码如下:

long square(volatile int*ptr)

{
    int a;
    a = *ptr;
    return a*a;
}

3. sizeof

C++ sizeof用法 .

sizeof不是函数,更像一个特殊的宏,它是在编译阶段求值的。

cout<<sizeof(int)<<endl; // 32位机上int长度为4
cout<<sizeof(1==2)<<endl; // == 操作符返回bool类型,相当于 cout<<sizeof(bool)<<endl;
在编译阶段已经被翻译为:

cout<<4<<endl;
cout<<1<<endl;

这里有个陷阱,看下面的程序:

int a = 0;
cout<<sizeof(a=3)<<endl;
cout<<a<<endl;
  输出为什么是4,0而不是期望中的4,3???就在于sizeof在编译阶段处理的特性。由于sizeof不能被编译成机器码,所以sizeof作用范围内,也就是()里面的内容也不能被编译,而是被替换成类型。=操作符返回左操作数的类型,所以a=3相当于int,而代码也被替换为:
int a = 0;
cout<<4<<endl;
cout<<a<<endl;

sizeof有两种用法:

(1)sizeof(object)

也就是对对象使用sizeof,也可以写成sizeof object 的形式。

(2)sizeof(typename)

也就是对类型使用sizeof,注意这种情况下写成sizeof typename是非法的。

任何时候,加括号总是对的。

sizeof的指针问题 

考虑下面问题:

cout<<sizeof(string*)<<endl; // 4
cout<<sizeof(int*)<<endl; // 4
cout<<sizof(char****)<<endl; // 4
可以看到,不管是什么类型的指针,大小都是4的,因为指针就是32位的物理地址。

结论:只要是指针,大小就是4(64位机变成8字节)。 但是其指向的变量占用不同大小的存储空间。

C++ STL vector:sizeof(vector)

向函数传递数组的问题

#include <iostream>
using namespace std; 

int Sum(int i[])
{
 int sumofi = 0;
 for (int j = 0; j < sizeof(i)/sizeof(int); j++) //实际上,sizeof(i) = 4
 {
  sumofi += i[j];
 }
 return sumofi;
} 

int main()
{
 int allAges[6] = {21, 22, 22, 19, 34, 12};
 cout<<Sum(allAges)<<endl;
 system("pause");
 return 0;
}
Sum的本意是用sizeof得到数组的大小,然后求和。但是 实际上,传入自函数Sum的,只是一个int 类型的指针,所以sizeof(i)=4,而不是24,所以会产生错误的结果。解决这个问题的方法使是用指针或者引用。

int Sum(int (*i)[6])
{
 int sumofi = 0;
 for (int j = 0; j < sizeof(*i)/sizeof(int); j++) //sizeof(*i) = 24
 {
  sumofi += (*i)[j];
 }
 return sumofi;
} 

int main()
{
 int allAges[] = {21, 22, 22, 19, 34, 12};
 cout<<Sum(&allAges)<<endl;
 system("pause");
 return 0;
}
在这个Sum里,i是一个指向i[6]类型的指针,注意,这里不能用int Sum(int (*i)[])声明函数,而是必须指明要传入的数组的大小,不然sizeof(*i)无法计算。但是在这种情况下,再通过sizeof来计算数组大小已经没有意义了,因为此时大小是指定为6的。

此外,对函数使用sizeof,在编译阶段会被函数的返回值类型取代。

struct的sizeof问题 

例如,下面的结构各成员空间分配情况:

struct test  
{  
     char x1;  
     short x2;  
     float x3;  
     char x4;  
};  
结构的第一个成员x1,其偏移地址为0,占据了第1个字节。

第二个成员x2为short类型,其起始地址必须2字节对界,因此,编译器在x2和x1之间填充了一个空字节。

结构的第三个成员x3和第四个成员x4恰好落在其自然对界地址上,在它们前面不需要额外的填充字节。

在test结构中,成员x3要求4字节对界,是该结构所有成员中要求的最大对界单元,因而test结构的自然对界条件为4字节,编译器在成员x4后面填充了3个空字节。整个结构所占据空间为12字节。

因为对齐问题使结构体的sizeof变得比较复杂,看下面的例子:(默认对齐方式下) 

struct s1
{
 char a;
 double b;
 int c;
 char d; 
}; 

struct s2
{
 char a;
 char b;
 int c;
 double d;
}; 

cout<<sizeof(s1)<<endl; // 24
cout<<sizeof(s2)<<endl; // 16
对于s1,首先把a放到8的对界,假定是0,此时下一个空闲的地址是1,但是下一个元素d是double类型,要放到8的对界上,离1最接近的地址是8了,所以d被放在了8,此时下一个空闲地址变成了16,下一个元素c的对界是4,16可以满足,所以c放在了16,此时下一个空闲地址变成了20,下一个元素d需要对界1,也正好落在对界上,所以d放在了20,结构体在地址21处结束。由于s1的大小需要是8的倍数,所以21-23的空间被保留,s1的大小变成了24。

对于s2,首先把a放到8的对界,假定是0,此时下一个空闲地址是1,下一个元素的对界也是1,所以b摆放在1,下一个空闲地址变成了2;下一个元素c的对界是4,所以取离2最近的地址4摆放c,下一个空闲地址变成了8,下一个元素d的对界是8,所以d摆放在8,所有元素摆放完毕,结构体在15处结束,占用总空间为16,正好是8的倍数。

这里有个陷阱,对于结构体中的结构体成员,不要认为它的对齐方式就是他的大小,看下面的例子: 

struct s1
{
 char a[8];
}; 

struct s2
{
 double d;
}; 

struct s3
{
 s1 s;
 char a;
}; 

struct s4
{
 s2 s;
 char a; 
}; 

cout<<sizeof(s1)<<endl; // 8
cout<<sizeof(s2)<<endl; // 8
cout<<sizeof(s3)<<endl; // 9
cout<<sizeof(s4)<<endl; // 16;
s1和s2大小虽然都是8,但是s1的对齐方式是1,s2是8(double),所以在s3和s4中才有这样的差异。

所以,在自己定义结构体的时候,如果空间紧张的话,最好考虑对齐因素来排列结构体里的元素。 

-------------------------------------------- 易错题 ----------------------------------------------

1. 用预处理指令#define声明一个常数,用以表明一年中有多少秒。

这是一个简单宏定义,考察三点:

(1)#define不能以分号结尾,括号使用。

(2)预处理器将为你计算常数表达式的值,因此写入计算式子比计算结果重要。

(3)意识到16位机将有一个整形数溢出,因此使用长整形,因此用到长整形符号UL。

答案:#define SECORES_PRE_YEAR (60*60*24*365) UL

2. 写一个标准宏,返回两个数较小那个。

这是一个带参数的宏,需要用到三元运算符。

答案:#define MIN(a,b) (a<=b ? a : b)

3. Const与define的区别?

二者都可以定义常量,区别在两点:

const常量有数据类型,宏常量没有数据类型。前者可进行安全检查,后者只进行字符替换,可能产生边际效应。

Const节省内存空间,存在编译器中,省去多余存取时间。

C++程序中Const常量完全取代宏常量。

C中默认const是外部连接,C迫使程序员使用#define。C++中默认const是内部连接,如果要定义外部连接,需要用extern const bufsize; //只声称

例如:const buffsize=100; char buf[bufsize];  看起来是对的,但实际上buffsize占用了一部分内存,C编译器并不知道它在编译时候的值。可以选择这样写:

const buffsize;

内部连接和外部连接

4. sizeof与strlen的区别

sizeof度量内存空间大小,strlen度量char*指向的字符串长度而且必须以‘\0’结尾;

sizeof是运算符,strlen是函数;

当使用结构类型或者变量时,sizeof返回实际大小,使用静态空间时,返回全部数组的尺寸(计算结构变量大小必须考虑对齐问题);

sizeof不能用于函数类型、不完全类型(未知存储大小、void、未知内容)、或位字段。

例1: char ss[100]=“0123456789”;

sizeof(ss)结果为400;strlen(ss)结果为11。

例2:int ss[100]="0123456789";

sizeof(ss)结果为400;strlen(ss)错误,参数不是char类型。

例3:char* ss[100]=“0123456789”;//ss是指针

sizeof(ss)结果为4,ss是指针,指针在32位系统大小固定为4B。

sizeof(*ss)结果为1,ss指针指向的第一个字符。

例4:char ss[ ]=“0123456789”;//ss是数组

sizeof(ss)结果为11,ss是数组,计算到“\0”位置。

sizeof(*ss)结果为1,*ss是第一个字符。

5.sizeof用途:

与存储分配和I/O系统类似例程进行通信;

查看某种类型的对象在内存中所占的单元字节;

给出动态分配内存的大小;

便于类型扩充。

6. 内联函数和宏定义的区别.

(1)内联函数可以加快程序运行。因为不需要中断调用,而宏只是一个简单的替换。

(2)内联函数要做参数类型检查。

内联函数适用于:一个函数不断地被调用;函数只有简单的几行。

你可能感兴趣的:(C++程序员面试宝典——预处理、counst与sizeof)