C++类
this指针
当一个类对象(或者指向类对象的指针)调用成员函数时,编译器会生成额外的代码,将对象的首地址(this指针)隐式的传给被调用的成员函数,使得在成员函数的内部能够访问到类对象的数据成员.
下面举例观察下this在成员函数调用时传递过程:
class CTest { int m_nNum; public: void print(); CTest(int nNum); }; CTest::CTest(int nNum) :m_nNum(nNum) { } void CTest::print() { printf("%d", m_nNum); } int main() { CTest t(1); t.print(); return 0; }
main函数对应汇编代码如下:
CTest t(1);
003914B8 push 1
003914BA lea ecx,[t]
003914BD call CTest::CTest (03910B4h)
t.print();
003914C2 lea ecx,[t]
003914C5 call CTest::print (0391172h)
return 0;
003914CA xor eax,eax
}
可以看出CTest的构造函数和成员函数print的被调用前的最后一条指令lea ecx,[t]把
对象的地址取出并放到ecx寄存器中,来看看构造函数的汇编代码:
003913D0 push ebp 003913D1 mov ebp,esp 003913D3 sub esp,0CCh 003913D9 push ebx 003913DA push esi 003913DB push edi 003913DC push ecx 003913DD lea edi,[ebp-0CCh] 003913E3 mov ecx,33h 003913E8 mov eax,0CCCCCCCCh 003913ED rep stos dword ptr es:[edi] 003913EF pop ecx 003913F0 mov dword ptr [this],ecx 003913F3 mov eax,dword ptr [this] 003913F6 mov ecx,dword ptr [nNum] 003913F9 mov dword ptr [eax],ecx 003913FB mov eax,dword ptr [this] 003913FE pop edi 003913FF pop esi 00391400 pop ebx 00391401 mov esp,ebp 00391403 pop ebp 00391404 ret 4
mov dword ptr [this],ecx 这句将调用对象的首地址赋值给this,
this指针也位于当前调用函数的栈帧上:
print的汇编代码:
00391420 push ebp 00391421 mov ebp,esp 00391423 sub esp,0CCh 00391429 push ebx 0039142A push esi 0039142B push edi 0039142C push ecx 0039142D lea edi,[ebp-0CCh] 00391433 mov ecx,33h 00391438 mov eax,0CCCCCCCCh 0039143D rep stos dword ptr es:[edi] 0039143F pop ecx 00391440 mov dword ptr [this],ecx 00391443 mov esi,esp 00391445 mov eax,dword ptr [this] 00391448 mov ecx,dword ptr [eax] 0039144A push ecx 0039144B push 395858h 00391450 call dword ptr ds:[399114h] 00391456 add esp,8 00391459 cmp esi,esp 0039145B call __RTC_CheckEsp (039113Bh) 00391460 pop edi 00391461 pop esi 00391462 pop ebx 00391463 add esp,0CCh 00391469 cmp ebp,esp 0039146B call __RTC_CheckEsp (039113Bh) 00391470 mov esp,ebp 00391472 pop ebp 00391473 ret
mov dword ptr [this],ecx
mov eax,dword ptr [this]
mov ecx,dword ptr [eax]
第一条指令是将调用对象的首地址赋值给this指针,因为CTest类中没有虚函数,所以数据成
员m_nNum的内存地址与类对象的首地址重合, mov eax,dword ptr [this]和
mov ecx,dword ptr [eax] 指令就可以将调用对象的m_nNum数据成员取出。
至此是不是对this指针有一个清晰认识呢?其实上面的C++代码和下面的C代码类似:
struct Test { int m_nNum; }; void Construct(Test obj,int nNum) { obj.m_nNum = nNum; } void print(Test obj) { printf("%d", obj.m_nNum); }
构造函数:
构造函数特点:构造函数函数名和类名相同,构造函数没有返回值,构造函数不能显示调用。
默认的构造函数:当类中没有声明构造函数时,C++标准规定编译器要为类定义一个默认的构造函数,
当类中声明了构造函数,编译器不再生成默认的,默认的构造函数啥也不干
注:VC++编译器一般不会为一个没有声明构造函数的类生成默认的构造函数,除非这个类中有继承关系
或者类成员是一个类对象。
析构函数:
析构函数特点:函数名为”~类名”,无返回值,
构造函数作用:当类对象生命周期到达终点时做一些清理工作,释放占用的资源
构造函数和析构函数的调用时机:
- 局部对象:在定义时调用普通构造函数,在出作用域时调用析构函数
- 全局对象:在main函数之前调用普通构造函数,在main函数结束之后调用析构函数,生命周期几乎和进程一样长
- 对象作为函数参数:
在进如函数之前传参时调用拷贝构造函数,在函数返回时调用析构函数。
注意:如果类中没有定义拷贝构造函数,编译器将自动生成默认的拷贝构造函数,其行为和memcpy相似,当类成员 中有指向动态分配资源的指针时,默认的拷贝构造函数是浅拷贝,仅仅复制了指针值,并没有申请资源,在 析构时可能会导致重复释放问题,因此需要自己实现拷贝拷贝构造函数。
VC++无名对象的优化
-
当函数的参数传入一个无名对象时,会优化掉拷贝构造函数的调用.
例:class CTest { int nTest; public: CTest() { std::cout << "CTest()" << std::endl; std::cout << "this:" << std::hex << this << std::endl; std::cout << "\r\n" << std::endl; } CTest(int n) { std::cout << "CTest(int n)" << std::endl; std::cout << "this:" << std::hex << this << std::endl; std::cout << "\r\n" << std::endl; } CTest(const CTest &) { std::cout << "CTest(const CTest &)" << std::endl; std::cout << "this:" << std::hex << this << std::endl; std::cout << "\r\n" << std::endl; } ~CTest() { std::cout << "~CTest()" << std::endl; std::cout << "this:" << std::hex << this << std::endl; std::cout << "\r\n" << std::endl; } };
进行如下测试:
void funTest1(CTest t) { return; } int main(int argc, char **argv) { CTest t(2); funTest1(t); std::cout << "====================" << std::endl; funTest1(CTest(2)); return 0; }
运行结果:
第一种情况下,先构造一个对象,然后将此对象作为参数传递给funTest1函数,传参过程中调用如果调用在传参数过程中调用了类的拷贝构造函数;
第二种情况下,在调用funTest1函数时,我使用CTest(int n)构造了一个无名对象作为参数传递,此时VC++编译器为了优化,不在生成调用拷贝构造函数的代码; -
函数返回值为无名对象时,也会优化掉拷贝构造函数的调用,函数调用完成后如果有接收返回值的代码,那么无名对象
的生命周期被延长,如果没有接收返回对象的代码,那么这个无名对象在这行代码执行完成后就被析构了例:
CTest funTest2() { return CTest(1); } int main(int argc, char **argv) { CTest t = funTest2(); std::cout << "==================" << std::endl; funTest2(); system("pause"); return 0; }
运行结果:
可以看出并未调用拷贝构造函数,第二次调用funTest2()时由于没有接收返回对象,无名对象就被析构了
const类对象
-
在定义类对象时,可以使用关键字const修饰,表示这个对象是个常量,该对象的数据成员一经构造函数的初始化 后,便不能再在经过其它成员函数来修改数据成员
-
const类对象只是编译器在编译时保证其数据数据成员不被修改,但是运行时还是可以修改其数据成员的值
-
const类对象不能调用普通成员函数,只能调用使用const关键字修饰的常量成员函数,因为成员函数的调用时会隐 式传递this指针,但指向const类对象的指针是const classtype类型的,而普通类对象的this指针是classtype
类型的,在传递this指针时,const classtype类型指针是无法转换为classtype类型指针 -
const成员函数
如果一个成员函数不应该修改对象的数据成员,那么可以在函数尾部使用const修饰;const类对象可以调用const
成员函数,非const类对象也可以调用const成员函数
const数据成员
如果类的常量数据成员只能在构造函数的初始化列表中进行初始化
使用mutable修饰的可变数据成员
该数据成员可以在任意的成员函数中修改,即使该函数是常量成员函数
静态成员变量
定义方式:在定义类的数据成员时使用static关键字修饰即可
初始化方式:在源文件中单独初始化,不需要再次使用static关键字
静态成员变量本质:受类域限制的全局变量,减少了和全局变量的名称冲突所有类对象共享一个静态成员变量,
静态成员变量存放在数据区中,所以静态成员变量不参与类大小的计算
静态成员函数
定义方式:在函数的返回值前面加上static关键字即可,静态成员函数是类的组成部分,被限制在类域中
注意:静态成员函数调用时不会隐式地传递this指针,所有只要通过"类名::静态函数名称"这种方式即可调用