几道C++笔试题,看看你的功底如何

发信人: NetMD (C++ is nothing), 信区: CPlusPlus
标 题: Re: 我来出几道C++笔试题,大家仔细想想看
发信站: 水木社区 (Tue Oct 24 17:27:30 2006), 站内


【 在 NetMD (C++ is nothing) 的大作中提到: 】
: 想了很久才想出这几道题,都是为了说明某些可能碰到的乍一看似乎很合理但
: 是实际上却很灵异的问题,大家仔细想想看,希望能够吸取某些教训在开发中
: 以避免这类问题:)
: 也欢迎其它朋友出题!
: 1。在目前的标准C++中,下面4个选项哪些一定是语法错误?其中T表示某个cv-
: unqualified类型。
首先感谢puke指出本题的一个漏洞,在这里把它补上:“T是cv-unqualified类型”
这个题出成选择题的确不是很好,也许当初题目改成“如果有语法错误请指出,否
则请说明语句执行结果”比较理想
: A)std::string str = "abc" + 'd';
"abc"类型是const char [4],'d'的类型是char(C语言中则是int),const char
[4]被转换成const char *,char被提升到int,然后const char *加上一个int则
是指针偏移运算,这个语句没有语法错误!
虽然这个语句没有语法错误,但是其结果却远非一般人理解的str的内容是"abcd",
'd'的ASCII值是100,因为"abc" + 'd'就是在.rodata段中"abc"保存的位置加上100
所得到的新地址,用这个地址来构造str,str的内容将是从这个地址开始一直到某
个内容为0的字符串,一般来说,这个内容将灵异的让人难以置信!
: B)const T& cref = NULL;
由于C++标准允许const引用绑定到一个匿名临时对象,因此该语句的作用就是用NULL
隐式构造一个临时对象T然后绑定到cref上,根据C++标准,NULL的值可以适应指针参
数以及标量参数
由于题干要求是选出一定有语法错误的答案,那么很显然,当T可以从NULL隐式构造
一个临时对象的时候,这个表达式并没有语法错误
这个语句(特别是用了NULL而不是0)有时候会被某些人误以为引用可以悬空,实际
上这个理解是错误的。引用一定要初始化并且只能初始化一次
特别的,std::basic_string<T>可以用字符串指针来构造,但是标准规定了这个指针
的值必须不能为NULL,因此如果T是string的时候将带来运行时错误!
: C)T& ref = NULL;
NULL一定是个rvalue,用NULL构造出来的临时对象也将是rvalue,C++标准规定rvalue
不能绑定到non-const引用,因此这个语句语法错误
注:事先声明T是cv-unqualified类型,否则这里的确是个漏洞,比如
typedef const int T;
: D)char *p = "AbC";
: p[1] = 'B';
C++标准规定"AbC"的类型是const char [4],但是为了兼容C,允许它被隐式转换为
char *,通过char *来修改指向的char语法上是允许的,但是实际上.rodata段不允
许被修改,所以又会带来运行时错误
注:C++标准规定字符串文本常量隐式转换到non-const字符指针这一行为被
deprecated,但是鉴于兼容legacy code的繁重任务,目前还没没有编译器这么做
[quote
D.4 Implicit conversion from const strings [depr.string]
The implicit conversion from const to non-const qualification for string
literals (4.2) is deprecated.
end quote]
: 2。结构体foo定义如下
: struct foo {
: short a;
: char b;
: char c;
: int d;
: };
: 在某32位little-endian平台上,已知sizeof(short)为2,定义一个foo对象如下:
: foo f = {1234, 'x', 'y', ('A' << 16) + ('B' << 8) + 'C'};
: 若cout << &f.d << endl;输出0x22ff8c,那么cout << &f.b << endl;将输出什么?
: btw,我承认这题非常无聊-_-
这道题用了大量的铺垫来吸引大家往alignment方向思考问题,实际上它只是一个
重载解析问题,cout有两个operator<<重载参数类型分别是const void *和const
char *,由于任何对象指针都可以隐式cast到void *或者const void *等等,非
char *的指针被解析到const void *输出一个指针值,而char *则被更准确的解
析到const char *输出一个'\0'结尾的字符串
原题中由于是little-endian机器,特意把int成员d构造成有一个byte为0的值,因
此输出了"xyCBA"
遇到这种灵异运行结果,比较好的debug方式是单步运行看看到底执行了什么代码,
然后分析问题并解决问题,直接从人的思维有时候很难发现原因,比如前面的那个
"abc" + 'd',太多人有着太高级的分析思维了,可惜编译器只能根据几条语法很
死板的处理,它们并没有那么好的容错性
: 3。写一个类bar,要求bar的对象只能从堆上分配,包括对象数组。
这个题用来作为C++的面试题比较好,算是个半开放问题,通过不断的提示和尝试
可以充分考察一个人的C++基本功,因为看似简单的需求包含了太多的语义要求

首先,这个题考查了对ctor和dtor的理解:一个对象分配在栈上的途径是直接构造
或者拷贝构造在栈上,退出函数的时候还需要调用dtor来析构对象,也就是说,构
造和析构两个语义缺一不可!
既然如此,那么就可以用access control来破坏这个语义需求:让ctor或者dtor不
可被访问,然后提供相应的Create或者Destroy方法来补偿缺失的语义!但是到底应
该non-public哪个呢?是ctor还是dtor呢?到底应该private还是应该protected呢?

显然,很多人会想到简单的把dtor给non-public了,理由很简单也很有效,那就是
dtor只有一个!不过这很快就会带来一个问题,因为new[] operator需要访问dtor
(后面会解释原因),单纯的non-public一个dtor会导致申请对象数组失败。
既然简单的把dtor限制住会带来麻烦,那么就把ctor给限制住吧,虽然这样麻烦了
一点,谁让出题的人bt到要申请数组呢!但实际上,限制ctor也没有那么简单,首
先一个就是原题并没有说明bar的作用,因此很多人可能不会考虑到copy ctor的影
响,而如果没有显式的声明copy ctor,编译器会合成一个public inline的copy
ctor,这将导致我们的需求出现缺口,因为我们可以先从堆上new一个对象,然后
拷贝构造给栈上的一个对象,反正dtor是可用的,这样并没有语法错误;除了copy
ctor的影响,还有一个问题就是default ctor也是个麻烦事,如果一个类提供了任
何一个ctor,编译器就不会合成一个default ctor,而如果这个类没有提供一个可
以无参数调用的ctor(无参数或者所有的参数都有默认值),new一个数组也会导
致失败,因为new[] operator不能带初始化参数(不考虑placement new)!
关于ctor和dtor还有一个问题在于,non-public到底应该处理成proteced还是private
呢,这取决于对类继承的需要和理解,由于派生类需要访问基类的某个ctor以及dtor,
因此private将阻碍类继承!

正因为有了申请对象数组的要求,单纯的将dtor给non-public是不可取的,那么就
选择将所有的ctor包括copy ctor以及default ctor都给non-public了吧,然后提供
一对static成员函数分别创建单个对象和数组(即所谓的工厂方法),如下
class bar {
bar() {}
bar(const bar& rhs) {}
/*由于dtor不是non-public,因此任何ctor都不能放过*/
public:
static bar *CreateSingle() {return new bar();}
static bar *CreateArray(size_t n) {return new bar[n];}

void Init(/*...*/) {/*initialize the bar object*/}
};
基本上,这样已经可以很好的运作的

下面我们来考虑更深远一点的问题,嗯,也就是比较虚一点的问题!很显然,由于
dtor是public的,任何一个ctor的遗漏都会导致需求的失败;另外,CreateXXXX这
种接口也导致了分配/回收的不对称,特别的,当它们的实现被当作黑盒的时候,
到底应该用delete还是delete[]回收或者其他形式的回收可能只能被文档来约束了。
由于dtor是public的,一个bar *指针既可以被delete也可以被delete[],这有时候
也是错误的根源!
要解决这两个问题,应该怎么办?比较简单的做法是把dtor也给non-public了,然
后提供一对回收操作DestroyXXXX来析构和回收对象!嗯,这里面又有一个无聊的细
节了,DestroyXXXX应该是static成员函数还是普通成员函数呢?大部分人很快就能
发现,这其实无所谓,都可以,CreateXXXX只能是static成员函数而DestroyXXXX却
不一定必须。当然,保持一致性,那就干脆也搞成static成员函数吧,把指针显式
传入DestroyXXXX函数!接下来就会有一个不见得被所有人能想到的问题了,那就是
这个指针应该是const bar *还是bar *呢,很多人可能习惯的就写成了bar *,这就
会导致下列代码不能通过,必须使用const_cast才可以
const bar *p = bar::CreateSingle();
bar::DestroySingle(p); // 错误,必须写成bar::DestroySingle((bar *)p);
原因在于bar *可以适应const bar *参数,但是反过来不可以!
有了上面的讨论,得到的bar设计为
class bar {
~bar() {}
bar() {}
bar(const bar& rhs) {}
/*为了保证bar的用户使用CreateXXXX来创建对象,还是应该不放过所有ctor*/

public:
static bar *CreateSingle() {return new bar();}
static bar *CreateArray(size_t n) {return new bar[n];}
static void DestroySingle(const bar *p) {delete p;}
static void DestroyArray(const bar *p) {delete[] p;}
/*注,如果DestroyXXXX是non-static成员函数,一定要是const成员函数*/

void Init(/*...*/) {/*initialize the bar object*/}
};
类似bar *和const bar*的讨论,如果DestroyXXXX是non-const成员函数,那么应该
是const成员函数

最后,来一点吹毛求疵,bar的用户可能会使用CreateSingle来创建对象却使用
DestroyArray来释放,或者反过来使用CreateArray来创建却使用DestroySingle来释
放,编译器并不能检查出来这个错误,怎么办?下面这样不是一个完美的方案,或者
说没有很完美的方案了
class bar {
~bar() {}
bar() {}
bar(const bar& rhs) {}
/*为了保证bar的用户使用CreateXXXX来创建对象,还是应该不放过所有ctor*/

public:
static bar *Create(size_t n) {return new bar[n];}
static void Destroy(const bar *p) {delete[] p;}
/*注,如果Destroy是non-static成员函数,一定要是const成员函数*/

void Init(/*...*/) {/*initialize the bar object*/}
};
也就是说,即使申请一个bar对象,我们也采用new[]的方式,这样对返回值的使用
并没有什么影响,创建和回收也已经匹配了,唯一的缺点就是创建单个bar对象的时
候也会纪录创建的对象个数,这带来了一定的空间损失!

最后,补充一下为什么dtor为non-public的时候new[]会失败:
new bar[n]的语义是先分配足够大的内存,然后构造n个bar对象并且返回首地址。
如果构造到m个(m <= n)的时候遇到了一个异常,则需要将已经构造好的所有对
象都析构掉然后回收内存最后rethrow该异常通知程序员。
因此,new[]是需要生成代码来调用dtor的!

很多人把这题理解成为对operator new/delete/new[]/delete[]的考察,其实是属
于对new/delete/new[]/delete[] operator和operator new/delete/new[]/delete[]
的语义没有分清楚,可以参考一下More Effective C++ item8

--
IUnknown ○
┏━━┻┓
INet ○┫ ┃
IM ○┫ NetMD┃
ID ○┫ ┃
┗━━━┛


※ 来源:·水木社区 newsmth.net·[FROM: 159.226.228.*]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值