1,C++中的空类,默认产生哪些类成员函数?
class Empty
{
public:
Empty(); // 缺省构造函数
Empty( const Empty& ); // 拷贝构造函数
~Empty(); // 析构函数
Empty& operator=( const Empty& ); // 赋值运算符
Empty* operator&(); // 取址运算符
const Empty* operator&() const; // 取址运算符 const
};
2,关于类型转换
static_cast、dynamic_cast、reinterpret_cast、和const_cast
关于强制类型转换的问题,很多书都讨论过,写的最详细的是C++ 之父的《C++ 的设计和演化》。最好的解决方法就是不要使用C风格的强制类型转换,而是使用标准C++的类型转换符:static_cast, dynamic_cast。标准C++中有四个类型转换符:static_cast、dynamic_cast、reinterpret_cast、和const_cast。下面对它们一一进行介绍。
static_cast
用法:static_cast < type-id > ( expression )
该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:
用于类层次结构中基类和子类之间指针或引用的转换。进行上行转换(把子类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类指针或引用转换成子类表示)时,由于没有动态类型检查,所以是不安全的。
用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
把空指针转换成目标类型的空指针。
把任何类型的表达式转换成void类型。
注意:static_cast不能转换掉expression的const、volitale、或者__unaligned属性。
dynamic_cast
用法:dynamic_cast < type-id > ( expression )
该 运算符把expression转换成type-id类型的对象。Type-id必须是类的指针、类的引用或者void *;如果type-id是类指针类型,那么expression也必须是一个指针,如果type-id是一个引用,那么expression也必须是一个 引用。
dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。
在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。
class B{
public:
int m_iNum;
virtual void foo();
};
class D:public B{
public:
char *m_szName[100];
};
void func(B *pb){
D *pd1 = static_cast<D *>(pb);
D *pd2 = dynamic_cast<D *>(pb);
}
在 上面的代码段中,如果pb指向一个D类型的对象,pd1和pd2是一样的,并且对这两个指针执行D类型的任何操作都是安全的;但是,如果pb指向的是一个 B类型的对象,那么pd1将是一个指向该对象的指针,对它进行D类型的操作将是不安全的(如访问m_szName),而pd2将是一个空指针。另外要注 意:B要有虚函数,否则会编译出错;static_cast则没有这个限制。这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表 (关于虚函数表的概念,详细可见<Inside c++ object model>)中,只有定义了虚函数的类才有虚函数表,没有定义虚函数的类是没有虚函数表的。
另外,dynamic_cast还支持交叉转换(cross cast)。如下代码所示。
class A{
public:
int m_iNum;
virtual void f(){}
};
class B:public A{
};
class D:public A{
};
void foo(){
B *pb = new B;
pb->m_iNum = 100;
D *pd1 = static_cast<D *>(pb); //copile error
D *pd2 = dynamic_cast<D *>(pb); //pd2 is NULL
delete pb;
}
在函数foo中,使用static_cast进行转换是不被允许的,将在编译时出错;而使用 dynamic_cast的转换则是允许的,结果是空指针。
reinpreter_cast
用法:reinpreter_cast<type-id> (expression)
type-id必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。
该运算符的用法比较多。
const_cast
用法:const_cast<type_id> (expression)
该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。
常量指针被转化成非常量指针,并且仍然指向原来的对象;常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象被转换成非常量对象。
Voiatile和const类试。举如下一例:
class B{
public:
int m_iNum;
}
void foo(){
const B b1;
b1.m_iNum = 100; //comile error
B b2 = const_cast<B>(b1);
b2. m_iNum = 200; //fine
}
上面的代码编译时会报错,因为b1是一个常量对象,不能对它进行改变;使用const_cast把它转换成一个常量对象,就可以对它的数据成员任意改变。注意:b1和b2是两个不同的对象。
3,关于派生类的构造函数,赋值函数等的说明
a, 派生类的构造函数应在其初始化表里调用基类的构造函数。
b, 基类与派生类的析构函数应该为虚(即加virtual关键字)。
c, 在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值。例如
class Base
{
public:
…
Base&operate=(const Base&other);
private:
int m_i,m_j,m_k;
};
class Derived:public Base
{
public:
Derived&operate=(const Derived&other);
private:
int m_x,m_y,m_z;
};
Derived&Derived::operate=(const Derived&other
{
//(1)检查自赋值
if(this==&other)
return*this;
//(2)对基类的数据成员重新赋值
Base::operate=(other);//因为不能直接操作私有数据成员
//(3)对派生类的数据成员赋值
m_x=other.m_x;
m_y=other.m_y;
m_z=other.m_z;
//(4)返回本对象的引用
return*this;
}
4,关于指针,数组,*,[],优先级结合问题
声明以下变量:
a) 一个指向有10个整型数数组的指针( A pointer to an array of 10 integers)
b) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to a function that takes an integer as an argument and returns an integer)
c)一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数( An array of ten pointers to functions that take an integer argument and return an integer )
解答:
a) int (*p)[10]; // A pointer to an array of 10 integers
b) int (*p)(int); // A pointer to a function a that takes an integer argument and returns an integer
c) int (*p[10])(int);
解析:
首先,int * p[10]与int (*p)[10]的区别。由于[]的结合优先级高于*,在 int * p[10]中致使p先与[]结合,表明p是一个数组名,其内容是int*。而在int (*p)[10]中p先与*结合表明p是一个指针,其指向一个无名数组,p值改变后,该数组将丢失。b,c的分析同类。
5,为什么不能有虚拟构造函数?
虚拟调用是一种能够在给定信息不完全(given partial information)的情况下工作
的机制。特别地,虚拟允许我们调用某个函数,对于这个函数,仅仅知道它的接口,而不知
道具体的对象类型。但是要建立一个对象,你必须拥有完全的信息。特别地,你需要知道要
建立的对象的具体类型。因此,对构造函数的调用不可能是虚拟的。拷贝构造函数本质和构造函数一样。
6,0字节问题
c++空类对象占用空间非0,一种解释是:方便寻址。假想两个连续定义的空类对象,如果为其分配0空间,将导致两个对象的地址相同,此时如对两空类对象分别寻址,寻得的地址亦相同,显然这会产生语义上的错误,因为不知该地址上的对象是何类型。对于malloc(0)这样的语句返回问题,则依赖于实现,C99标准并未定义实现,它可能返回一个NULL,也可能返回一个有效地址,有效的地址不代表有效的空间,返回有效地址,其有0字节空间,也是可行的。内存分配操作是建立在与操作对应的数据结构基础之上,只要数据结构定义合理并操作得当,如何处理0字节空间不是问题。
7,关于可重入与线程安全
若一个程序或子程序可以安全的被并行执行,则称其为可重入(reentrant或re-entrant)的;即,当该子程序正在运行时,可以再次进入并执行它。若一个函数是可重入的,则该函数:
不能含有静态(全局)非常量数据。 不能返回静态(全局)非常量数据的地址。 只能处理由调用者提供的数据。 不能依赖于单实例模式资源的锁。 不能调用不可重入的函数。 多'用户/对象/进程优先级'以及多进程一般会使得对可重入代码的控制变得复杂。同时,IO代码通常不是可重入的,因为他们依赖于像磁盘这样共享的、单独的资源。
可重入就是,一个函数没有执行完成,由于外部因素或内部调用,又一次进入该函数执行。可重入代码,必须保证资源的互不影响的使用,比如全局变量,系统资源等。
在LINUX设备驱动中 关于可重入代码:
简单介绍,因为驱动能够被多个进程调用,互不干扰,这样驱动必须是可重入的。
可重入与线程安全两个概念都关系到函数处理资源的方式。但是,他们有一定的区别。可重入概念会影响函数的外部接口,而线程安全只关心函数的实现。
可重入最简单的理解就是任何变量都是局部变量。可重入指函数在运行过程中,被中断打断后,待返回时仍然能够正常运行。这就需要在编写代码时注意全局变量和公用资源的使用,同时还需要有编译器的支持。
大多数情况下,要将不可重入函数改为可重入的,需要修改函数接口,使得所有的数据都通过函数的调用者提供。 要将非线程安全的函数改为线程安全的,则只需要修改函数的实现部分。一般通过加入同步机制以保护共享的资源,使之不会被几个进程同时访问。 因此,相对线程安全来说,可重入性是更基本的特性,它可以保证线程安全:即,所有的可重入函数都是线程安全的,但并非所有的线程安全函数都是可重入的。
可重入性是函数编程语言的关键特性之一。
8,关于volatile与中断
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:
并行设备的硬件寄存器(如:状态寄存器)
一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
多线程应用中被几个任务共享的变量
a)一个参数既可以是const还可以是volatile吗?解释为什么。
b)一个指针可以是volatile 吗?解释为什么。
c)下面的函数有什么错误:
int square(volatile int *ptr)
{
return *ptr * *ptr;
}
下面是答案:
a)是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
b)是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。
c)这段代码有点变态。这段代码的目的是用来返指针*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;
}
中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展—让标准C支持中断。具代表事实是,产生了一个新的关键字__interrupt。下面的代码就使用了__interrupt关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码:
__interrupt double compute_area (double radius)
{
double area = PI * radius * radius;
printf("/nArea = %f", area);
return area;
}
错误如下:
a)ISR 不能返回值。
b)ISR 不能传递参数。
c)在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的。
d)与第三点一脉相承,printf()经常有重入和性能上的问题。