Programming Languages
调用惯例
C的调用惯例是Caller clean-up的,
也就是说Caller负责将压入栈的参数弹出栈。
因此,C支持可变参数的函数,如prinf
。
参数压栈的顺序是逆序的,即最后一个参数最先入栈。
整型和指针型返回值是存在特定寄存器里的。
void _cdecl funct();
syscall
调用和_cdecl
的调用惯例是相似的,用于OS/2的API。
Callee clean-up的调用惯例有pascal
和stdcall
,
函数自己清理栈上的参数,
因此,可以省去很多代码,不需要再每次调用结束后清理栈。
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;