Saturday, July 10, 2004

 

Some C++ interview questions - 1

1.
From http://blog.csdn.net/longwycn/archive/2005/01/18/257651.aspx
http://blog.csdn.net/longwycn/archive/2005/01/18/257655.aspx

一、(共10分,每题1分)单项选择

1. 对于全局函数 int f(void) ,与其完全等价的函数原型为:

A. int& f( ); B. int f(void ) const; C. const int f( ); D. A、B、C都不是

2. 类A中有唯一的非静态成员函数int f(A& one)和私有的数据成员int x;,那么在此成员函数的函数体中可以有语句:

A. return one.f(this); B. x=f(this); C. this->f(x.one); D. int* const pX=&one.x;

3. 下面关于new 和delete操作符的说法,哪个是不正确的:

A. 使用new操作符,可以动态分配全局堆中的内存资源。

B. 若p的类型已由A*强制转换为void *,那么执行语句delete p;时,类A的析构函数不会被调用。

C. 实现全局函数时,new和delete通常成对地出现在由一对匹配的花括号限定的语句块中。

D. 执行语句A * p=new A[100];时,类A的构造函数只会被调用1次。

4. 类TM和类TN是两个独立的类,那么类TM中哪种形式的数据成员一定是不允许的:

A. TM* a; B. TN& a; C. TM a; D. TN a;

5. 类B是通过public继承方式从类A派生而来的,且类A和类B都有完整的实现代码,那么下列说法正确的是:

A. 类B中具有pubic可访问性的成员函数个数一定不少于类A中public成员函数的个数。

B. 一个类B的实例对象占用的内存空间一定不少于一个类A的实例对象占用的内存空间。

C. 只要类B中的构造函数都是public的,在main函数中就可以创建类B的实例对象。

D. 类A和类B中的同名虚函数的返回值类型必须完全一致。

6. 下列哪种函数可以是虚的:

A. 自定义的构造函数 B. 拷贝构造函数 C. 静态成员函数 D. 析构函数

7. 类A有一个实例化的常量对象a,那么下面的说法中不正确的是:

A. 类A中的非静态数据成员一定都是常量成员。

B. 通过a可以直接调用类A的常量成员函数。

C. a不能直接作为左值表达式使用。

D. a可以是静态常量对象。

8. 在不考虑强制类型转换的情况下,关于类中常量成员函数的下列说法不正确的是:

A. 常量成员函数中不能修改本类中的非静态数据成员。

B. 常量成员函数中可以调用本类中的任何静态成员函数。

C. 常量成员函数的返回值只能是void或常量。

D. 若常量成员函数中调用虚函数f,那么函数f在本类中也一定是一个常量成员函数。

9. 类C是以多重继承的方式从类A和类B继承而来的,类A和类B无公共的基类,那么:

A. 类C的继承方式只能采用public继承。 B. 可改用单继承的方式实现类C的同样功能。

C. 类A和类B至少有一个是抽象类。 D. 类A和类B至少有一个是虚基类。

10. 下列哪种用法不能体现类A和类B之间的组合关系:

A. 类A中声明数据成员B b;

B. 类A中声明数据成员B* b;

C. 类A中声明数据成员const B& b;

D. 类A中声明成员函数 B func( B& obj);

二、(共10分,每题2分,多选、少选、错选都不得分)多项选择

1. 下面关于指针变量和引用变量的说法正确的是:

A. 指针变量存放的是内存地址,并且可以置为0。

B. 定义引用变量时,必须同时指明具体被引用的对象或变量。

C. 使用取地址操作符,可以取得指针变量自身的地址,但取不到引用变量自身的地址。

D. 类中的数据成员可以是指针变量,但不能是引用变量。

2. 类A中有唯一的一个公有成员函数f,对于类A的一个对象a,执行语句 a.f(100);成功,那么f 的函数原型可以为:

A. A& f( int, int=50); B.void f(int&) const; C.const A * f(const int ); D. A f( const int&) const;

3. 关于类中重载赋值运算符的正确说法是:

A. 由编译器提供的缺省赋值函数具有public访问性并且执行按位赋值。

B. 在重载派生类的赋值操作时,不但要实现派生类中数据成员的赋值,还要负责基类中数据成员的赋值。

C. 只有在类中含有指针数据成员或引用数据成员时,才需要重载类的赋值操作。

D. 通过修改类A的声明或定义,可以禁止用户在类A对象间进行任何赋值操作。

4. 在类的静态成员函数的实现体中,可以访问或调用:

A. 本类中的静态数据成员 B. 本类中非静态的常量数据成员

C. 本类中其它的静态成员函数 D. 本类中非静态的成员函数

5. 关于异常和C++提供的异常处理机制正确的说法是:

A. 若程序员预见到程序中的异常,则一定可以通过修改程序代码来避免异常出现。

B. 使用dynamic_cast操作符可能会产生异常。

C. 异常可以用catch进行捕获处理。

D. 异常可以是对象,也可以是普通整数。

三、(共20分,每题2分)判断正误,对于你认为错误的论述,说明原因或举出反例。

1. 函数体内声明的静态变量,至多只会被初始化一次。

2. 对于同一个类中的两个非静态成员函数,它们的函数的名称、参数类型、参数个数、参数顺序以及返回值的类型不能完全相同。

3. inline函数表示编译器一定会将函数体直接插入到调用此函数的地方,这样可以加快程序的运行速度。

4. 每个cpp文件是单独编译的,但一个程序中多个cpp文件的编译顺序是不固定的。

5. 将类A所有构造函数的可访问性都限制为私有的,那么在main函数中不可能得到类A的实例对象。

6. 名字空间是可以多层嵌套的;对于类A中的函数成员和数据成员,它们都属于类名A代表的一层名字空间。

7. 若在类Base和派生类Derived中,分别声明一个同名的整型成员变量int x; ,那么通过Derived类的实例对象,不可能输出基类Base中的变量x的值。

8. 若类A是类B的友员类,且类B是类C的友员类,那么类A也是类C的友员类。

9. 虽然抽象类的析构函数可以是纯虚函数,但要实例化其派生类对象,仍必须提供抽象基类中析构函数的函数体。

10. 构造函数的初始化列表中的内容,不会影响构造对象中成员数据的初始化顺序。

四、(5分)写出下面程序的运行结果

#include

class A {

public:

A(int v1)(v1) { f( ); }

virtual ~A( ) { f( ); }

virtual void f( ) {++count; x+=10;Show( );}

virtual void Show ( ) const

{cout<<"count="<< count<<" x="<< x<< endl;}

protected:

static int count;

private:

int x;

};

class B:public A {

public:
B(int v1,int v2):A(v1),y(v2) { f( ); }

virtual ~B( ) { f( ); }

virtual void f( ) {++count;y+=50; Show ( );}

virtual void Show ( ) const

{cout<<"count="<< count<<" y="<< y<< endl;}

private:

int y;

};

int A::count=0;

int main( )

{ A* p=new B(1,2);

p->f( );

delete p;

return 0;

}


五、(5分)写出下面程序的运行结果

#include

class A {

private:

friend class B;

A(int val=0):data(val),use(1) { }

int use, data;

};

class B {

public:

B(int val=0):pa(new A(val)) { }

~B( ) { if(--pa->use==0) delete pa; }

B(const B& rhs) {++rhs->use;pa=rhs.pa;}

B& operator=(const B& rhs);

A* operator->( ) const {return pa;}

void Display( ) const { cout<<"data="<data

<<"use="<use<
private:
A * pa;

};
B& B::operator=(const B& rhs)

{

if (this==&rhs) return *this;

if (--pa->use==0) delete pa;

++rhs->use;

pa=rhs.pa;

return *this;

}

int main() {

B b1(1),b2(b1),b3(3);

b1.Display();

b2.Display();

b3.Display();

b3=b1;

b1.Display();

b3.Display();

return 0;

}


六、(共20分)回答下列各题

1. 说明类中的public、 protected和 private成员函数分别在public、protected和private继承方式下在派生类中的可访问性。(4分)

2. 若类A和类B没有继承关系,对于函数void func(A&) ,请至少用两种不同方法说明如何才能传递一个非常量的B类对象给func函数。(4分)

3. 在声明类中的构造函数、析构函数、纯虚函数、静态成员函数、非静态的成员函数以及全局函数作为类的友元函数时,哪些可以是常量成员函数?(3分)

4. 举例说明重载(overload) 、重定义(redefine) 和重写(override)的含义。(3分)

5. 通常类A的拷贝构造函数的原型写为A(const A&);,请问为什么参数一定要使用引用形式?使用const修饰符除了可以防止修改传递给构造函数的实参外,还有什么作用?(3分)

6. 程序员规范中要求不要写出类似(++i)+(i++) 或 f(++i,i++)这样的代码,请说明原因。(3分)

七、(共8分)阅读下面部分程序代码,完成3个问题。

#include

class A {

public: A(int v1)(v1) {}

virtual int GetData( ) const {return x;}

private: int x;

};

class B:public A {

public: B(int v1,int v2):A(v1),y(v2) {}

virtual int GetData( ) const {return y;}

private: int y;

};
class C:public B {

public: C(int v1,int v2,int v3):B(v1,v2),z(v3) {}

virtual int GetData( ) const {return z;}

private: int z;

};

const A& max2(const A& one,const A& two)

{ return (one>two?one:two); }

void main() {

B b(1,2); C c(100,10,1);

cout<< max2(b,c).GetData( )<< endl;

cout<< max2(b,5).GetData( )<< endl;

}


1) (4分)请在不改变上述已有代码的条件下,添加必要的函数使得程序可以编译通过和运行,并输出结果2和5。其中比较对象时只比较各自的或在派生类定义的数据成员,如比较类B的对象b和类C的对象c时,实际比较的是b.y和c.z。

2) (2分) 若不改变函数max2的函数体,只将函数声明改成A& max2(const A& one,const A& two),请问这样行吗?为什么?

3) (2分)若main中有语句A* p=new C(1,2,3);和delete p;,那么类A、类B和类C应做何修改?

八、(共10分)现考虑编写一个扑克游戏:只用一付无大小王的扑克,扑克的花色(suit)分为Spade、Heart、Diamond和Club,每门花色的牌共13张,面值(rank)分别为2、3、4、5、6、7、8、9、10、Jack、Queen、King和Ace,每张扑克牌应包含如下信息:唯一的ID号(1-52)、花色、面值、背面图案的编号等。每张扑克牌的操作有:判断两张扑克牌是相同花色吗? 相同面值吗?判断一张扑克牌是给定的花色吗?是给定的面值吗?请写出扑克牌类Card类的声明和实现代码,要求选取适当形式的数据成员描述每张扑克牌的信息,同时以成员函数的形式实现指定的操作。

九、(共12分)现有一个2x2的矩阵,其第一行元素从左到右依次为a1和a2,第二行元素从左到右依次为a3和a4,简记此矩阵为M(a1,a2,a3,a4)。对矩阵M可进行两种操作:一种是转置,即交换a2和a3的位置;另一种是变换,即将矩阵M(a1,a2,a3,a4)变换为M(b1,b2,b3,b4),其中bi是变换ai后的结果。假定已有全局函数int GetNextPrime(int data); 它返回比data大的最小素数,若返回值越界则返回2。不用考虑模板,请按下列具体要求实现代码。

1)(6分)若ai都是整数,每次变换都将ai变换成比ai大的最小素数(i=1,2,3,4)。请用类M描述满足上述条件的矩阵,要求给出类M的声明和实现代码,同时写出main函数,并在其中创建一个M(1,10,100,1000)矩阵。

2)(6分)若矩阵中元素a2和a3既可以是整数,也可以是另一个满足此定义的矩阵,例如:矩阵M(1, M(2, 3, M(4,5,6,7),8 ,9 ,10)。矩阵转置时,既要交换a2和a3的位置,还要对a2和a3中的矩阵进行转置;矩阵变换时,同样也要对其中的矩阵元素进行变换。 请给出满足此要求的矩阵类M的声明和实现代码。

answer

一、单选
1.C 2.D 3.D 4.C 5.B 6.D 7.A 8.C 9.B 10.D

二、多选
1. ABC
2. ACD
3. ABD
4. AC
5. BCD

三、判断对错
1. 对
2. 错 如可有两个成员函数,int f();和 int f() const;
3. 错 是否真正内联,由编译器决定。如函数是递归函数,那么永远不会内联。
4. 对
5. 错 可通过类的静态成员函数创建实例,如单件模式。
6. 对
7. 错 如可通过继承自基类的成员函数访问基类中的x.
8. 错 友员关系不具有传递性。
9. 对
10.对

四、写结果。 目的是考察虚函数的调用,包括普通成员函数中调用其它虚函数和
在构造、析构函数中只会调用虚函数的本地版本。

五、写结果。 是一个使用Handle类进行引用计数的例子。

六、回答问题
1. 重点是基类中的任何Private在派生类中都是不可访问的。
2. 可在A类中定义一个构造函数:A(const B&);
或在B类中定义一个自动转换函数: operator A( ) const;
3. 只有纯虚函数、非静态的成员函数可以是常量成员函数。
(这题有点问题,析构函数可以是纯虚函数,但不能是常量函数)
4. 如类A中有函数 int f(void);和int f(int);为重载(overload) (同名,参数不同)
如类A中有函数 int f(void);,A的派生类B中给int f(void)一个新的实现体,为redefine
如类A中有虚函数 virtual int f(void);,A的派生类B中给virtual int f(void)一个新的实现体,
为override.
5. A(const A& one ).当调用此拷贝构造函数时,需将参数压栈,若不使用&,就需要在
栈内创建一个one的副本,而这需要用拷贝构造函数。这样就会形成递归调用。
使用const,还允许用一个常量对象作为样本,来构造一个新的对象。
6. 计算子表达式的顺序由编译器决定的,虽然参数的压栈顺序在给定的调用方式下式固定的,
但参数表达式的计算顺序也由编译器决定的。不同的编译器或不同的表达式计算的顺序可能
不一致。

七.
1. 添加全局函数
bool operator>(const A& one, const A& two)
{
return one.GetData()>two.GetData();
}
2. 不行。 return (one>two?one:two); 时,不能将常量赋给变量。
3. 需要将ABC三个类的析构函数改为虚的。

八. Card类(不用enum,只用数字也可以)

main函数略

//enum前加上const就可将下面所有的都放入.h中。
const enum SUIT {SPADE=0,HEART,DIAMOND,CLUB};
const enum RANK {TWO=0,THREE,FOUR,FIVE,SIX,SEVEN,EIGHT,NINE,TEN,JACK,QUEEN,KING,ACE};
class Card
{
public:
Card(int id):mID(id),mSuit((id-1)/13),mRank((id-1)%13){}
bool IsSameSuit(const Card& rhs)
{ return ((this==&rhs)?false:mSuit==rhs.mSuit);}
bool IsSameRank(const Card& rhs)
{ return ((this==&rhs)?false:mRank==rhs.mRank); }
bool IsSuit(int suit)
{return mSuit==suit;}
bool IsRank(int rank)
{return mRank==rank;}
private:
static int nBackImg; //背面图案
const int mID;
const int mSuit;
const int mRank;
};


九.
1) class M
{
public:
M(int v1,int v2,int v3,int v4):a1(v1),a2(v2),a3(v3),a4(c4) { }
void Turn( ) { int temp=a2; a2=a3; a3=temp;}
void Trans( ) {
a1=GetNextPrime(a1);
a2=GetNextPrime(a2);
a3=GetNextPrime(a3);
a4=GetNextPrime(a4);
}
private:
int a1,a2,a3,a4;
};

2)(因没有讲授设计模式部分,可以采用变通的方法,例如)
class M
{
public:
M(int v1,int v2,int v3,int v4)
:a1(v1),a2(v2),a3(v3),a4(v4), p2(0),p3(0){ }
M(int v1,int v2,M* pt3,int v4)
:a1(v1),a2(v2),a3(0),a4(v4),p2(0),p3(pt3) { }
M(int v1,M* pt2,int v3,int v4)
:a1(v1),a2(0),a3(v3),a4(v4),p2(pt2),p3(0) { }
M(int v1,M* pt2,M *pt3,int v4)
:a1(v1),a2(0),a3(0),a4(v4),p2(pt2),p3(pt3) { }
void Turn( )
{
int temp=a2; a2=a3; a3=temp;
M* p=p2; p2=p3; p3=p;
if (p2) p2->Turn();
if (p3) p3->Turn();
}
void Trans( ) {
a1=GetNextPrime(a1);
if (p2)
p2->Trans();
else
a2=GetNextPrime(a2);

if (p3)
p3->Trans();
else
a3=GetNextPrime(a3);
a4=GetNextPrime(a4);
}
private:
int a1,a2,a3,a4;
M* p2;
M* p3;
};

2.
C++关键字(static/register/atuo/extern/volatile/const)释疑
From http://www.csdn.net/develop/read_article.asp?id=21103

选择自 njustcxy 的 Blog

下面关于C++的几个关键字是经常和我们打交道的而我们又经常对这些含糊不清的,本文根据自己的学习体会作以总结,以期达到真正理解和活用的目的。

static
l 静态变量作用范围在一个文件内,程序开始时分配空间,结束时释放空间,默认初始化为0,使用时可改变其值。

l 静态变量或静态函数,即只有本文件内的代码才可访问它,它的名字(变量名或函数名)在其它文件中不可见。

l 在函数体内生成的静态变量它的值也只能维持

int max_so_far( int curr )//求至今(本次调用)为止最大值

{

static int biggest; //该变量保持着每次调用时的最新值,它的有效期等于整个程序的有效期

if( curr > biggest )

biggest = curr;

return biggest;

}

l 在C++类的成员变量被声明为static(称为静态成员变量),意味着它为该类的所有实例所共享,也就是说当某个类的实例修改了该静态成员变量,其修改值为该类的其它所有实例所见;而类的静态成员函数也只能访问静态成员(变量或函数)。

l 类的静态成员变量必须在声明它的文件范围内进行初始化才能使用,private类型的也不例外。如,
float SavingsAccount::currentRate = 0.00154;
(注:currentRate是类SavingsAccount的静态成员变量)
register
l 用register声明的变量称着寄存器变量,在可能的情况下会直接存放在机器的寄存器中;但对32位编译器不起作用,当global optimizations(全局优化)开的时候,它会做出选择是否放在自己的寄存器中;不过其它与register关键字有关的其它符号都对32位编译器有效。

auto
l 它是存储类型标识符,表明变量(自动)具有本地范围,块范围的变量声明(如for循环体内的变量声明)默认为auto存储类型。

extern
l 声明变量或函数为外部链接,即该变量或函数名在其它文件中可见。被其修饰的变量(外部变量)是静态分配空间的,即程序开始时分配,结束时释放。用其声明的变量或函数应该在别的文件或同一文件的其它地方定义(实现)。在文件内声明一个变量或函数默认为可被外部使用。

l 在C++中,还可用来指定使用另一语言进行链接,这时需要与特定的转换符一起使用。目前Microsoft C/C++仅支持”C”转换标记,来支持C编译器链接。使用这种情况有两种形式:

u extern “C” 声明语句

u extern “C” { 声明语句块 }

volatile
l 限定一个对象可被外部进程(操作系统、硬件或并发线程等)改变,声明时的语法如下:

int volatile nVint;

这样的声明是不能达到最高效的,因为它们的值随时会改变,系统在需要时会经常读写这个对象的值。 只常用于像中断处理程序之类的异步进程进行内存单元访问。

const
l const所修饰的对象或变量不能被改变,修饰函数时,该函数不能改变在该函数外面声明的变量也不能调用任何非const函数。在函数的声明与定义时都要加上const,放在函数参数列表的最后一个括号后。

l 在C++中,用const声明一个变量,意味着该变量就是一个带类型的常量,可以代替#define,且比#define多一个类型信息,且它执行内链接,可放在头文件中声明;但在C中,其声明则必须放在源文件(即.C文件)中,在C中const声明一个变量,除了不能改变其值外,它仍是一具变量,如

const int maxarray = 255;

char store_char[maxarray]; //C++中合法,C中不合法

l const修饰指针时要特别注意。例:

char *const aptr = mybuf; // 常量指针
*aptr = 'a'; // Legal
aptr = yourbuf; // Error
const char *bptr = mybuf; // (指针bptr)指向常量数据
*bptr = 'a'; // Error
bptr = yourbuf; // Legal
l const修饰成员函数时不能用于构造和析构函数。

3.
重载、覆盖与隐藏
By wcp872123
From http://blog.csdn.net/wcp872123/archive/2005/02/03/279119.aspx

重载与覆盖
成员函数被重载的特征:
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual关键字可有可无。
覆盖是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual关键字。

“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,
规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。
此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。
此时,基类的函数被隐藏(注意别与覆盖混淆)。

如下示例程序中:
(1)函数Derived::f(float)覆盖了Base::f(float)。
(2)函数Derived::g(int)隐藏了Base::g(float),而不是重载。
(3)函数Derived::h(float)隐藏了Base::h(float),而不是覆盖。

#include

class Base{
public:
virtual void f(floatx){cout<< "Base::f(float)"<< x<< endl;}
void g(floatx){cout<< "Base::g(float)"<< x<< endl;
void h(floatx){cout<< "Base::h(float)"<< x<< endl;}
};

class Derived:publicBase{
public:
virtual void f(floatx){cout<< "Derived::f(float)"<< x<< endl;}
void g(intx){cout<< "Derived::g(int)"<< x<< endl;}
void h(floatx){cout<< "Derived::h(float)"<< x<< endl;}
};
void main(void){
Derived d;
Base *pb=&d;
Derived *pd=&d;

//Good:behavior depends solely on type of the object
pb->f(3.14f); //Derived::f(float)3.14
pd->f(3.14f); //Derived::f(float)3.14

//Bad:behavior depends on type of the pointer
pb->g(3.14f); //Base::g(float)3.14
pd->g(3.14f); //Derived::g(int)3(surprise!)

//Bad:behavior depends on type of the pointer
pb->h(3.14f); //Base::h(float)3.14(surprise!)
pd->h(3.14f); //Derived::h(float)3.14
}

4.
C++中几个比较不常用的关键字

From http://www.donews.net/xzwenlan/archive/2004/12/25/214166.aspx

mutable关键字

关键字mutable是C++中一个不常用的关键字,他只能用于类的非静态和非常量数据成员
我们知道一个对象的状态由该对象的非静态数据成员决定,所以随着数据成员的改变,
对像的状态也会随之发生变化!

如果一个类的成员函数被声明为const类型,表示该函数不会改变对象的状态,也就是
该函数不会修改类的非静态数据成员.但是有些时候需要在该类函数中对类的数据成员
进行赋值.这个时候就需要用到mutable关键字了

例如:
class Demo
{
public:
Demo(){}
~Demo(){}
public:
bool getFlag() const
{
m_nAccess++;
return m_bFlag;
}
private:
int m_nAccess;
bool m_bFlag;
};

int main()
{
return 0;
}

编译上面的代码会出现 error C2166: l-value specifies const object的错误
说明在const类型的函数中改变了类的非静态数据成员.

这个时候需要使用mutable来修饰一下要在const成员函数中改变的非静态数据成员
m_nAccess,代码如下:

class Demo
{
public:
Demo(){}
~Demo(){}
public:
bool getFlag() const
{
m_nAccess++;
return m_bFlag;
}
private:
mutable int m_nAccess;
bool m_bFlag;
};

int main()
{
return 0;
}

这样再重新编译的时候就不会出现错误了!

volatile关键字

volatile是c/c++中一个鲜为人知的关键字,该关键字告诉编译器不要持有变量的临时拷贝,它可以适用于基础类型
如:int,char,long......也适用于C的结构和C++的类。当对结构或者类对象使用volatile修饰的时候,结构或者
类的所有成员都会被视为volatile.

使用volatile并不会否定对CRITICAL_SECTION,Mutex,Event等同步对象的需要
例如:
int i;
i = i + 3;
无论如何,总是会有一小段时间,i会被放在一个寄存器中,因为算术运算只能在寄存器中进行。一般来说,volatitle
关键字适用于行与行之间,而不是放在行内。

我们先来实现一个简单的函数,来观察一下由编译器产生出来的汇编代码中的不足之处,并观察volatile关键字如何修正
这个不足之处。在这个函数体内存在一个busy loop(所谓busy loop也叫做busy waits,是一种高度浪费CPU时间的循环方法)

void getKey(char* pch)
{
while (*pch == 0)
;
}

当你在VC开发环境中将最优化选项都关闭之后,编译这个程序,将获得以下结果(汇编代码)
; while (*pch == 0)
$L27
; Load the address stored in pch
mov eax, DWORD PTR _pch$[ebp]
; Load the character into the EAX register
movsx eax, BYTE PTR [eax]
; Compare the value to zero
test eax, eax
; If not zero, exit loop
jne $L28
;
jmp $L27
$L28
;}

这段没有优化的代码不断的载入适当的地址,载入地址中的内容,测试结果。效率相当的低,但是结果非常准确

现在我们再来看看将编译器的所有最优化选项开关都打开以后,重新编译程序,生成的汇编代码,和上面的代码
比较一下有什么不同
;{
; Load the address stored in pch
mov eax, DWORD PTR _pch$[esp-4]
; Load the character into the AL register
movsx al, BYTE PTR [eax]
; while (*pch == 0)
; Compare the value in the AL register to zero
test al, al
; If still zero, try again
je SHORT $L84
;
;}

从代码的长度就可以看出来,比没有优化的情况要短的多。需要注意的是编译器把MOV指令放到了循环之外。这在
单线程中是一个非常好的优化,但是,在多线程应用程序中,如果另一个线程改变了变量的值,则循环永远不会
结束。被测试的值永远被放在寄存器中,所以该段代码在多线程的情况下,存在一个巨大的BUG。解决方法是重新
写一次getKey函数,并把参数pch声明为volatile,代码如下:

void getKey(volatile char* pch)
{
while (*pch == 0)
;
}

这次的修改对于非最优化的版本没有任何影响,下面请看最优化后的结果:

;{
; Load the address stored in pch
mov eax, DWORD PTR _pch$[esp-4]
; while (*pch == 0)
$L84:
; Directly compare the value to zero
cmp BYTE PTR [eax], 0
; If still zero, try again
je SHORT $L84
;
;}

这次的修改结果比较完美,地址不会改变,所以地址声明被移动到循环之外。地址内容是volatile,所以每次循环
之中它不断的被重新检查。

把一个const volatile变量作为参数传递给函数是合法的。如此的声明意味着函数不能改变变量的值,但是变量的
值却可以被另一个线程在任何时间改变掉。

explicit关键字

我们在编写应用程序的时候explicit关键字基本上是很少使用,它的作用是"禁止单参数构造函数"被用于自动型别转换,
其中比较典型的例子就是容器类型,在这种类型的构造函数中你可以将初始长度作为参数传递给构造函数.
例如:
你可以声明这样一个构造函数
class Array
{
public:
explicit Array(int size);
......
};
在这里explicit关键字起着至关重要的作用,如果没有这个关键字的话,这个构造函数有能力将int转换成Array.一旦这种
情况发生,你可以给Array支派一个整数值而不会引起任何的问题,比如:
Array arr;
...
arr = 40;
此时,C++的自动型别转换会把40转换成拥有40个元素的Array,并且指派给arr变量,这个结果根本就不是我们想要的结果.如果
我们将构造函数声明为explicit,上面的赋值操作就会导致编译器报错,使我们可以及时发现错误.
需要注意的是:explicit同样也能阻止"以赋值语法进行带有转型操作的初始化";
例如:
Array arr(40);//正确
Array arr = 40;//错误

看一下以下两种操作:
X x;
Y y(x);//显式类型转换
另一种
X x;
Y y = x;//隐式类型转换

这两种操作存在一个小小的差别,第一种方式式通过显式类型转换,根据型别x产生了型别Y的新对象;第二种方式通过隐式转换
产生了一个型别Y的新对象.
explicit关键字的应用主要就是上面所说的构造函数定义种,参考该关键字的应用可以看看STL源代码,其中大量使用了该关键字

__based关键字

该关键字主要用来解决一些和共享内存有关的问题,它允许指针被定义为从某一点开始算的32位偏移值,而不是内存种的绝对位置
举个例子:

typedef struct tagDEMOSTRUCT {
int a;
char sz[10];
} DEMOSTRUCT, * PDEMOSTRUCT;

HANDLE hFileMapping = CreateFileMapping(...);
LPVOID lpShare = (LPDWORD)MapViewOfFile(...);

DEMOSTRUCT __based(lpShare)* lpDemo;

上面的例子声明了一个指针lpDemo,内部储存的是从lpShare开始的偏移值,也就是lpHead是以lpShare为基准的偏移值.
上面的例子种的DEMOSTRUCT只是随便定义的一个结构,用来代表任意的结构.

虽然__based指针使用起来非常容易,但是,你必须在效率上付出一定的代价.每当你用__based指针处理数据,CPU都必须
为它加上基地址,才能指向真正的位置.

在这里我只是介绍了几个并不时很常见的关键字的意义即用法,其他那些常见的关键字介绍他们的文章已经不少了在这里
就不再一一介绍了.希望这些内容能对大家有一定的帮助!



<< Home

This page is powered by Blogger. Isn't yours?