Programming Languages

调用惯例

C的调用惯例是Caller clean-up的, 也就是说Caller负责将压入栈的参数弹出栈。 因此,C支持可变参数的函数,如prinf。 参数压栈的顺序是逆序的,即最后一个参数最先入栈。 整型和指针型返回值是存在特定寄存器里的。

void _cdecl funct();

syscall调用和_cdecl的调用惯例是相似的,用于OS/2的API。

Callee clean-up的调用惯例有pascalstdcall, 函数自己清理栈上的参数, 因此,可以省去很多代码,不需要再每次调用结束后清理栈。 x86指令ret可以带一个参数,即需要清理的栈长度。 pascal调用惯例是从左往右压参数入栈的。 而stdcall是从右往左逆序压栈的,这是Win32 API的调用惯例。

C++的调用惯例是thiscall。 对于GCC,几乎和_cdecl是一致的, 只是在压入所有参数后,再压入this指针, 因此,this指针就如同第一个参数一样。 对于微软的Visual C++编译器, thiscall则几乎与stdcall一致, 也就是由Callee来清理栈。 另外,微软的编译器将this指针放在寄存器上。

整型提升

由于最终的运算是以CPU的位长进行的, 因此,C语言设计为:低于int长度的类型在进行运算时, 都要先提升为整型int。 如果不能用int表示,则提升为unsigned int。 例如,unsigned short的取值范围可能大于int, 因此提升为unsigned int

前缀+运算的作用就是对运算符进行整型提升。 提升的方式是将原始值用新的类型表示, 因此,有无符号对提升的结果是有影响的。

char a = 0xb6;
printf("%08x,%d\n", (+a), a == 0xb6);
unsigned char b = 0xb6;
printf("%08x,%d\n", (+b), b == 0xb6);
/* 结果是
ffffffb6,0
000000b6,1
*/

内存对齐 (data structure alignment)

因为内存是一个字一个字的读取的,因此,对齐到字长的整倍数的地址的访问效率最高。 这是进行内存对齐的原因。 以n个字节为字长的对齐叫做n字节对齐(n-bytes aligned)。 这里的n是2的整数次幂。

对于结构体,每个成员按照其长度进行对齐, 比如,int是4字节对齐的,char是单字节对齐的,double是8字节对齐的。 为了对齐,可以在成员之间加入padding。 整个结构体,是按照最长的成员进行对齐的。 有的语言会调整成员顺序以使得对齐后占用的内存尽量少。 但是,C和C++是不允许编译器调整成员顺序的。

举个例子。

struct MixedData {
  char Data1; short Data2;
  int Data3; char Data4;};

在x86的32位机器上对齐之后,是这样的。

struct MixedData {
  char Data1; /* 1 byte */
  char Padding1[1]; /* 为了对齐Data2 */
  short Data2; /* 2 bytes */
  int Data3;  /* 4 bytes 最长的 */
  char Data4; /* 1 byte */
  char Padding2[3]; /* 为了对齐整个结构体 */
};

C++虚函数表和多重继承

C++标准并没有明确指定虚函数表的实现方式。 通常,编译器会为每个类创建一个虚函数表(vtable, virtual method table), 并且为每个有虚函数的对象加入一个指向虚函数表的指针(vpointer, VPTR, virtual table pointer)。 编译器将这个指针作为对象的一个隐藏成员(第一个,或者最后一个), 并在对象的构造函数中加入隐藏的代码,初始化这个指针。

当有多重继承存在时,父类的成员都会成为派生类的成员。

class A { int a; virtual void func(){} };
class B { int b; virtual void func(){} };
class C : A, B { int c; }

上述类C会从A和B继承得到两个虚函数表指针。 因此,类C的对象内存布局如下。

vptr from A
int a
vptr from B
int b
int c

值得注意的是,对于多重继承的情况, 派生类指针在转为基类指针时,会加入一定的偏移量,以保证这个对象在基类看起来布局是正确的。 比如,C类对象的指针在转化为B类对象的指针时,就会增加sizeof(A)的偏移,使得这个C类对象看起来也是一个B类对象。 这在C++中叫做pointer fixups或者thunks。

C++类型转换

C++可以通过单参数构造函数和类型转换运算符来定义类型转换。 这样定义的类型转换可以用于 (1) C风格的类型转换,(2) 构造函数风格的类型转换, (3) static_cast静态类型转换,(4)函数参数的隐式类型转换。

class A {};
class B {
public:
  B() {}
  B(const A& a) { cout << "B(const A&)" << endl; }
  operator A() { cout << "operator A()" << endl; return A(); }
};
void fooa(A a) {}
void foob(B b) {}
int main()
{
  A a; B b;
  a = (A)b; a = A(b); fooa(b); a = static_cast<A>(b); // operator A()
  b = (B)a; b = B(a); foob(a); b = static_cast<B>(a); // B(const A&)
}

C++有几种显示类型转换语法。 static_cast完成静态类型转换,包括数值类型转换、指针转换、 调用单参数构造函数进行转换等。 dynamic_cast则可以利用对象的运行时类型信息, 在继承链上向下转换时,确保转换后的对象符合指定的类型。 下面的代码显示了两者的不同。

struct A { int a; };
struct B { int b; virtual void foo() {} }; // 多态类型
struct C : public A, public B { int c; };
int main()
{
  C c;
  // 以下四种方式都可以完成正确的向上转换
  cout << &(c) << endl; // c的地址
  cout << (B*)&c << endl; // c中类B部分的地址
  cout << static_cast<B*>(&c) << endl;
  cout << dynamic_cast<B*>(&c) << endl;
  B b;
  // 以下三种方式都不加检查的进行向下转换
  cout << &(b) << endl;
  cout << (C*)&b << endl;
  cout << static_cast<C*>(&b) << endl;
  // 由于b不是C的实例,因此一下转换结果为0
  cout << dynamic_cast<C*>(&b) << endl;
  return 0;
}

reinterpret_cast完全保留变量的二进制表示。 const_cast可以去除常量访问限制。

char * p = "hello";
// cout << static_cast<int>(p) << endl; 语法错误
// 以下三条语句输出的数值一致
cout << (void*)p << endl;
cout << hex << reinterpret_cast<int>(p) << endl;
cout << hex << (int)p << endl;