您的位置:首页 > 编程语言 > C语言/C++

编程启示录:一些语法的本质(C++)

2012-10-15 12:51 155 查看
其实我是标题党...
1 引用
许多书上都写着“C++的引用是变量的别名”,在语义上的确如此,但有时会造成引用不占内存的误解,实际上引用占用与指针相等大小的内存,当然这经常会得到优化,但指针也经常能得到优化。引用存在的意义在于在某些场合代替指针作为参数或返回值(不要返回局部变量的引用),常引用的意义在于可以引用不能取地址的常量,这些甚至是运算符重载所必需的,引用通常不应该作为类的成员或变量出现。

2 数组与指针
数组名代表第一个元素的地址,这并没有错误,但不能把数组与指针等同起来,数组是一种独立的数据类型,int[2]和int[4]也是不同的数据类型,数组不能作为参数和返回值出现,但我们却可以这样写int func(int param[3]),这是因为编译器会把出现在参数中的数组当做指针来处理,这等同于int func(int* param)。对于int func(int param[3][4])则与int
func(int (*param)[4])等同,但与int func(int** param)不同。

3 ++i++
对于类似于i=i++之类的表达式的结果都是不确定的,基本上只要一个表达式中一个变量出现了两次,但值发生了改变,那这个表达式的值就是不确定,即使加了括号依然如此,对于不同的编译器如此,即使是同一个编译器在不同位置出现,得到的结果也可能不同,如果不信服请看论坛C++置顶帖,总之讨论这类问题没有意义。

4 变量的声明
C++的变量声明有时令人费解,也有着许多的解释,对于int*p有人认为int*是一个整体,我之前也是这样认为,但其实这是错的。许多初学者只能理解像int (*p)[4],int*p[4]这样的声明,更复杂的声明就难以理解了。其实C++的变量声明是在模仿它的使用,例如int *((*p)[3])[4]表明*((*p)[i])[i]是int变量,那么按照运算符的顺序可以得到:

((*p)[i])[i]是int指针,

(*p)[i]是int指针的数组,

(*p)是int指针的数组的数组,

p是int指针的数组的数组的指针。

我认为写成int*[4][3]* p更清晰,但C++不是这么规定的。

对于const则是修饰他前面的那个符号,如果在开头才是修饰后面的符号。

对于引用则可以把&理解为解引用运算符(实际上并不存在)且与取地址运算符一个优先级,例如int &*a[3],表明对a进行下标运算,再取值,再解引用得到int,那么a的类型是int引用的指针的数组,这显然是不符合C++类型规定的。

5 sizeof
在首页上看到了亚马逊二面的面试题,实际上这类问题和i=i++是类似,答案都不确定,但是确实有必要了解一下编译器对于继承虚函数的实现以及内存对齐的处理。

内存对齐规则
任意内置类型type按照sizeof(type)对齐,也就是说必须放在偏移地址(offset)为sizeof(type)的整数倍的位置上。

数组type
的对齐方式与type相同。

结构体(类)与其对齐方式最大的成员的对齐方式相同。

结构体(类)的大小为其对齐方式的整数倍。

例如

struct A
{
char a;            //offset=0
short b[3][4];     //按照2对齐,所以offset=2
int c;             //2+3*4*2=26,但按照4对齐,所以offset=28
char d;            //offset=28+sizeof(int)=32
};                      //A已占用32+sizeof(char)=33的空间,按照对齐方式最大的c对齐,因此sizeof(A)=36,且A按照4对齐。
struct B
{
__int64 a; //offset=0
char b;     //offset=8
A c;        //A按照4对齐因此offset=12
};              //B按照8对齐,大小为48。


也可以通过#param pack指令来设置最大的对齐方式,声明“#param pack(n)”则与上文的规则相同若声明“#param pack(2)”则int,__int64,A都按照2字节对齐,因此sizeof(A)=32,sizeof(B)=42。

内存对齐的意义在于加快内存访问速度,而且某些cpu只支持2或4的整数倍寻址。

C++对象模型
首先将基类当作子类的成员。

空类(没有虚表)占一个字节的大小,子类可以和他的一个空基类合并,也就是说子类的一个空基类不再占用子类的空间,但多个空基类不会合并。

C++中如果一个类有虚函数或虚基类(包括通过继承得到的),那么这个类就会有一个指向虚表(vtbl)的虚表指针(vptr)。子类的虚表可以和它的一个基类的虚表合并,因此单继承中一个类只有一个虚表。

(以下代码并不规范)

虚函数的具体实现类似于:

struct base//基类
{
struct vtable
{
void (*pf1)();//函数指针
void (*pf2)();
}
void f1(){...};
void f2(){...};
static vtable vtbl={&f1,&f2};//虚表
basevtbl* vptr=&vtbl;//虚表指针
}
struct A
{
struct vtable
{
void (*pf1)();
void (*pf2)();
void (*pf3)();
}
void f1(){...};
void f2(){...};
void f3(){...};
static vtable vtbl={&f1,&f2,&f3};
basevtbl* vptr=&vtbl;
}
base* a=new A();
a->vtbl->pf1();//a->f1(),这样实现了调用A类的函数而不是base类的函数


虚继承:

虚继承中基类相对于子类的位置并不固定,因此要引入一个指向基类的指针,而且不能进行基类到子类的转换.

base与前面相同

struct A //class A:virtaul base
{
struct vtable
{
void (*pf1)();
void (*pf2)();
void (*pf3)();
base* pbase;
}
void f1(){...};
void f2(){...};
void f3(){...};
static vtable vtbl={&f1,&f2,&f3,&mybase};
base mybase; //base类的虚表也要改变,比较复杂,见下文.
}


对于多重继承和虚继承,由于基类不一定在子类的开头,因此基类的地址与子类不同,如果子类重写了虚函数,就要为基类单独生成一个虚表,其中的虚函数指针会指向一个新的函数,这个函数先将this指针调整为子类的位置,再调用子类的虚函数。

但是我在上面所写的只是一种实现,只要一个类含有虚函数或虚基类那么它的大小就不确定,虚表的位置不确定,这会对内存对其造成影响,虚函数表也可能与虚基类表分开,虚表或者空类也有可能不合并,总之可能性太多了,在不同的编译器上会有不同的结果,研究此类问题毫无意义,我不知道为什么会出这样的题。面试官要求输出vc的结果,你拿vc试一下不就知道了么,提供sizeof运算符是为了方便编程调试优化,提高C语言的表达能力,不是为了方便你们出这种无聊题目。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: