1.类的声明和定义
注意类的声明和定义分离的时候,在定义处要使用域作用限定符,否则函数声明链接时的定位不到函数的定义。
这些成员变量、函数的作用于这个类域,将功能集成在一起,这体现出封装的思想。
在区分类的定义和声明时,主要看有没有开辟空间
在.h文件中,成员变量和成员函数都算作声明,只有在创建这个类的时候,才会为成员变量创建空间。但是,成员函数并不会被拷贝。这样可以防止空间的浪费。
为形象理解,可以将类的声明视为图纸,这个图纸可以实例出多个对象。
因此在计算对象的大小时,只会计算成员变量的大小,同时满足和结构体一样的对齐操作,当然你也可以用#pragma pack(1)来设置默认对齐数。
注意这是64位平台测试,指针的大小是8个字节。
我们可以将成员函数视为类的公共区域,每个成员调用成员函数都是直接到类里面来找。注意刚刚的描述是便于形象理解成员函数的调用,和访问限定符(protected、struct默认公有public,class默认私有private,只要不写public都是私有)不同,不要混淆。
2.因空指针导致的程序崩溃
解释下面代码为什么不报编译错误:
我们需要通过编译的过程来解释这一现象:
预处理是将所有的宏和头文件展开,生成的文件我们仍然能读懂。
编译是检查语法错误,语义是否能被正确解读。在这里,nullptr的使用并没有导致语法的错误,也不会产生歧义,所以编译这里不会报错。
汇编是将所有代码转化为二进制的机器指令。
同样,在链接时也不会出现问题,生成可执行程序。
最终是因为越界访问导致程序崩溃。
注意导致程序崩溃的原因是对空指针的指向区域进行的访问或修改。如果不进行这些操作,那么就是可行的。引用就是个很好的例子。
因为引用的语法和实质层面的不一致,导致这里很容易被误解为对a进行了访问。但因为引用的实质是指针,所以这里只是将a的值nullptr赋给了b,并没有对nullptr产生访问行为,所以这里的程序不会有任何问题。
在观察程序崩溃时,先看语法层面上是否造成歧义,导致编译错误;再看要使用的函数是否都成功定义且定位,这关系到链接错误;最后从汇编的角度来看程序是否发生了越界访问等。
3.不同的this指针区分不同的对象
this是一个关键字。它是隐含在类中的一种指针,在对该类实例化出多个对象时,this指针就用来给每个对象贴上标签。
为了理解它,先看一段代码,解释为什么两次调用Add时都没有传参,但在类里面调用函数时还是能区分不同的对象:
#include <iostream>
using namespace std;
class C
{
public:
void Init(int a = 0, int b = 0, int c = 0)
{
_a = a, _b = b, _c = c;
}
void Add()
{
cout << "add:" << _a + _b + _c << endl;
}
private:
int _a;
int _b;
int _c;
};
int main()
{
C c1, c2;
c1.Init(1, 2, 3);
c2.Init(4, 5, 6);
c1.Add();
c2.Add();
return 0;
}
结果是:
其中虽然我们看上去没有传参,但是在汇编代码中我们就可以看到实际上Init和Add都多传了一个参数,这个参数就是this指针,在这里它的类型是C* const c1,C* const c2:
在代码语义上,它们相当于在参数中,多传了一个类的指针:
在实际写代码的时候,虽然this指针实际存在,但参数中却不能写this,因此这里使用注释来表示实际的执行情况。但是,在成员函数内部,可以显式写出this指针,因为this指针其实是类的指针,而类又和struct同源,所以用的是this->形式。
又因为this是成员的标签,它也不能随便更换其指向。
this指针可能存在栈中,也可能存在寄存器中,不同编译器有不同的做法。但this指针都不会存在成员中。这可以用空类来进行验证。
如果指针确实存在成员中,那么这个类的大小至少是8(64位),所以this指针是单独在栈或寄存器存放的。而空类的大小为1是作为创建类成功的标志,对于没有成员变量的类都是如此。
4.this指针为空指针的代码解读
先来看一段代码
#include <iostream>
using namespace std;
class C
{
public:
void Print()
{
cout << "Hello,world!" << endl;
}
private:
int _a;
};
int main()
{
C* c1 = nullptr;
c1->Print();
return 0;
}
运行结果是:
可以看出这个代码执行没有任何问题。
这要结合上面第2点对空指针的分析方法来判断。分析如下图: