五、类对象模型
遵循内存对齐原则,Stack类的成员变量是 a、size、capacity,每个变量的类型都是int,因此Stack类的成员变量大小为3*4=12个字节。
1. int main() 2. { 3. Stack st; 4. cout << sizeof(st) << endl; 5. 6. return 0; 7. }
但是,打印发现Stack类的的对象大小就是12,那成员函数没有计算大小吗?
假如对象中包含类的各个成员,既包含成员变量,又包含成员函数,由于每个对象中成员变量是不同的,但调用的是同一份成员函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份成员函数代码,相同代码保存多次,会浪费空间。
因此C++采用类对象中只存储成员变量,成员函数存放在公共代码段中的方式来存储类的对象。
1. #include<iostream> 2. using namespace std; 3. 4. class A1 { 5. public: 6. void f1() {} 7. private: 8. int _a; 9. }; 10. 11. // 类中仅有成员函数 12. class A2 { 13. public: 14. void f2() {} 15. }; 16. 17. // 类中什么都没有---空类 18. class A3 19. {}; 20. 21. int main() 22. { 23. A1 a1; 24. A2 a2; 25. A3 a3; 26. 27. cout << sizeof(a1) << endl; 28. cout << sizeof(a2) << endl; 29. cout << sizeof(a3) << endl; 30. 31. cout << &a1 << endl; 32. cout << &a2 << endl; 33. cout << &a3 << endl; 34. 35. return 0; 36. }
sizeof(a2) 和 sizeof(a3) 为什么都是1?编译器给空类一个字节来标识这是一个空类,这1个byte不是为了存储数据,是占位,表示对象存在过。尽管对象a2的类中没有成员变量仅有成员函数,对象a3的类中既没有成员变量也没有成员函数,但是它们的地址不同,表明对象a2和对象a3存在过。
当定义一个对象时,如何将对象初始化呢?C++把成员变量和成员函数封装到类里面,不想让成员变量随意被访问,需要通过公有的合法方式进行访问。下面的代码是无法给d1正确初始化的:
1. #include<iostream> 2. using namespace std; 3. 4. class Date 5. { 6. public: 7. void Init(int year, int month, int day) 8. { 9. year = year; 10. } 11. 12. private: 13. int year; 14. int month; 15. int day; 16. }; 17. 18. int main() 19. { 20. Date d1; 21. //d1.year = 2022;//非合法访问,类外面无法访问private成员变量 22. d1.Init(2021, 5, 20); 23. 24. return 0; 25. }
因为第9行year = year中的year都是第7行的入参year,因为所有的变量访问都遵循就近原则,类里面有两个变量year,一个是第8行的入参year,另一个是第13行的成员变量year,因此第9行代码中的赋值的两个year会优先访问第7行的入参year,都是第7行的入参year的值,把自己赋值给自己,没起任何作用。
虽然第9行可以指定域:
Date::year = year;
编译能够通过,但是写起来不方便,为此可以用命名风格进行区分,成员变量使用这样的风格:
1. private: 2. int _year; 3. int _month; 4. int _day;
也可以使用其他风格,只要和入参能够区分开就可以。因此,代码可以这样写:
1. #include<iostream> 2. using namespace std; 3. 4. class Date 5. { 6. public: 7. void Init(int year, int month, int day) 8. { 9. _year = year; 10. _month = month; 11. _day = day; 12. } 13. 14. private: 15. int _year; 16. int _month; 17. int _day; 18. }; 19. 20. int main() 21. { 22. Date d1; 23. d1.Init(2022, 4, 8); 24. 25. return 0; 26. }
F10监视对象d1的三个变量,发现被初始化成功了:
成员变量声明是没有为对象开辟空间的,只有定义了对象后,数据存储在对象里面
1. private: 2. //成员变量声明 3. int _year; 4. int _month; 5. int _day;
如果定义两个对象,d1和d2如何访问自己的年月日呢?两个对象调用的是同一份成员函数,如何做到各自初始化各自的?
1. int main() 2. { 3. Date d1; 4. d1.Init(2022, 4, 8); 5. 6. Date d2; 7. d2.Init(2022, 4, 9); 8. 9. return 0; 10. }
因为编译器做了处理,Init函数的入参不是3个参数,实际上是4个参数,编译器会增加一个隐含的参数:this指针,编译器会把对象的地址传给this。
1. int main() 2. { 3. Date d1; 4. d1.Init(2022, 4, 8);//实际为d1.Init(&d1,2022,4,8) 5. 6. Date d2; 7. d2.Init(2022, 4, 9);//实际为d2.Init(&d2,2022,4,9) 8. 9. return 0; 10. }
编译器增加的隐含的this指针参数,即void Init(Date* this, int year, int month, int day),作为形参,this指针存在于栈中。
this指针是隐含的,是编译器编译时显示自动增加的,不能显式地在调用和函数定义中增加,在Date类中第9-11行加不加this访问成员变量都能成功初始化,可以在成员函数中使用this指针。
使用this指针打印两个对象各自的地址:
1. #include<iostream> 2. using namespace std; 3. 4. class Date 5. { 6. public: 7. void Init(int year, int month, int day) 8. { 9. _year = year; 10. _month = month; 11. _day = day; 12. cout << "this:" << this << endl; 13. } 14. 15. private: 16. int _year; 17. int _month; 18. int _day; 19. }; 20. 21. int main() 22. { 23. Date d1; 24. d1.Init(2022, 4, 8); 25. cout << "d1:" << &d1 << endl; 26. 27. Date d2; 28. d2.Init(2022, 4, 9); 29. cout << "d2:" << &d2 << endl; 30. 31. return 0; 32. }
第一次调用Init,this的地址就是d1的地址,第二次调用Init,this的地址就是d2的地址:
为了加深对this指针的理解,可以看看下面代码:
1. #include<iostream> 2. using namespace std; 3. 4. // 1.下面程序能编译通过吗? 5. // 2.下面程序会崩溃吗?在哪里崩溃 6. 7. class A 8. { 9. public: 10. void PrintA() 11. { 12. cout << _a << endl; 13. } 14. void Show() 15. { 16. cout << "Show()" << endl; 17. } 18. 19. private: 20. int _a; 21. }; 22. 23. int main() 24. { 25. A* p = nullptr; 26. p->PrintA(); 27. p->Show(); 28. }
程序在26行p->PrintA( );处崩溃,可以注掉第26行,就可以打印Show()了。
为什么访问p->PrintA( )会崩溃?因为p为空指针,第10行的成员函数PrintA()接收到的指针是空指针,访问this->_a对空指针解引用会崩溃。
p->Show( )没有对p这个指针解引用,因为Show( )这个成员函数的地址没有存到对象里面,因此能够正常编译。