> 文章列表 > C++类和对象:类的定义、类对象的存储、this指针

C++类和对象:类的定义、类对象的存储、this指针

C++类和对象:类的定义、类对象的存储、this指针

目录

一. 对于面向过程和面向对象的认识

二. 类

2.1 struct关键字定义类

2.1.1 C语言中的struct关键字

2.1.2 C++中的struct关键字 

2.2 class关键字 

2.1 使用class关键字定义类

三. 类的访问限定及封装

3.1 类的访问权限及访问限定符

3.1.1 访问权限

3.1.2 访问限定符

3.2 封装

四. 类的作用域

五. 类对象的存储方式和大小计算

5.1 类对象在内存中的存储方式

5.2 空类的大小

六. this指针

6.1 什么是this指针

6.2 this指针的特性和使用规则

6.3 this指针的存储位置


一. 对于面向过程和面向对象的认识

  • C语言:是面向过程的,注重分析解决问题的每个细节过程,通过调用函数逐步解决问题。
  • C++:是面向对象的,关注的是对象,将一个问题拆分为不同的对象,依靠对象之间的交互来解决问题。

注意:严格来说,C++应该是面向对象和面向过程混编的语言,因为C++兼容C语言。

举个通俗的例子来理解什么是面向过程和面向对象。假设要设计一个简易的外卖系统:

  • 面向过程:关注下单、接单、送餐等过程,体现的代码层面就是方法和函数。
  • 面向对象:关注客户、骑手、商家等对象及他们之间的相互关系,体现到代码层面就是类的设计定义和类之间的关系。

二. 类

2.1 struct关键字定义类

2.1.1 C语言中的struct关键字

在C语言中,struct表示结构体,如演示代码2.1所示,通过struct声明了学生信息结构体类型变量,其中包含name、age两个结构体成员,在主函数中,定义结构体类型变量并初始化和打印。

演示代码2.1:

#include<stdio.h>struct stu
{char name[20];int age;
};int main()
{struct stu s = { "zhangsan", 25 };printf("name: %s, age: %d\\n", s.name, s.age);return 0;
}
图2.1 演示代码2.1的运行结果

2.1.2 C++中的struct关键字 

在C++中,struct被升级为了类,同时,struct类兼容C语言中结构体的用法。与C语言结构体不同,在struct类中,我们不仅可以定义成员变量,还可以定义函数。struct中定义的函数可以直接使用类的成员变量,一般来说,为了区分类的成员变量和成员函数的形参,我们在成员变量名称的前面加_,如:int _age 和 char _name[20],函数形参则通过不加‘_’来区分,如func(int age, char* name)。

在演示代码2.2中,首先定义结构体类型变量s1,通过C语言初始化结构体成员变量的方法将struct类中的成员变量初始化,然后调用成员函数Print来打印每个成员变量,以此来证明C++中的struct兼容C语言结构体的用法。然后,定义对象s2(定义类可以省略struct),通过语句s.Init("lisi", 30)来调用成员函数Init为s2的成员变量赋值,之后再次调用Print函数打印成员变量信息。

调用类成员变量的语法格式为:对象名.类成员函数(传参列表)。

演示代码2.2:

struct stu
{//成员变量char _name[20];int _age;//成员方法void Init(const char* name, int age)   //初始化成员变量函数{strcpy(_name, name);_age = age;}void Print()  //打印成员变量函数{cout << "姓名:" << _name << endl;cout << "年龄:" << _age << endl;}
};int main()
{//采用C语言的方法定义结构体类型变量s1并初始化其成员变量struct stu s1;strcpy(s1._name, "zhangsan");s1._age = 20;s1.Print();  //打印结构体成员变量//采用C++的方法定义对象s2,调用成员函数Init初始化成员变量stu s2;  //定义类可以省去structs2.Init("lisi", 30);s2.Print();return 0;
}
图2.2  演示代码2.2运行结果

2.2 class关键字 

2.1 使用class关键字定义类

在C++中,可以使用struct关键字定义类,也可以使用class关键字定义类。class定义的类与struct定义的类相似,其中可以定义成员变量和和成员函数。如演示代码2.3所示,定义了一个名为stu的类,其中包含成员变量name和age、成员函数Init和Print。

这是否说明在C++中,class定义类和struct定义类没有任何不同?显然不是。class定义的类的成员变量和成员方法的属性为私有属性,而struct定义的类为了兼容C语言结构体的用法,默认为公有属性。第三章中会对公有属性和私有属性进行讲解。

演示代码2.3:

class stu
{//成员变量char _name[20];int _age;//成员方法void Init(const char* name, int age)   //初始化成员变量函数{strcpy(_name, name);_age = age;}void Print()  //打印成员变量函数{cout << "姓名:" << _name << endl;cout << "年龄:" << _age << endl;}
};

三. 类的访问限定及封装

3.1 类的访问权限及访问限定符

3.1.1 访问权限

如果用演示代码3.1所示的主函数访问演示代码2.2中class定义的,会报出某些成员变量及成员函数无法访问的错误,这是为什么?那为什么演示代码2.2同样是在主函数中对类的成员进行访问,代码就能正常运行呢?这就涉及到访问权限的问题。

演示代码3.1:

class stu
{//成员变量char _name[20];int _age;//成员方法void Init(const char* name, int age)   //初始化成员变量函数{strcpy(_name, name);_age = age;}void Print()  //打印成员变量函数{cout << "姓名:" << _name << endl;cout << "年龄:" << _age << endl;}
};int main()
{stu s1;strcpy(s1._name, "zhangsan");s1._age = 20;s1.Print();  return 0;
}
图3.1  演示代码3.1运行结果

类的访问权限分三种:公有、私有、保护。

struct关键字定义的类默认访问权限是公有,可以在类的外部直接被访问。class定义的关键字默认访问权限为私有,无法在类的外部直接进行访问。

3.1.2 访问限定符

三种访问权限,对应三个访问限定符:private -- 私有、public -- 公有、protect -- 保护。我们可以通过使用三个访问限定符,来改变类的成员的属性,来决定类成员是否可以在类外部进行访问。

图3.2 访问限定符

演示代码3.2对2.2中定义的stu类使用访问限定符更改类成员的属性,将成员变量全部设为私有属性,将成员函数全部设为共有属性。这样,在主函数中,就能访问成员函数了,但还是不能访问成员变量。

演示代码3.2:

class stu
{
private://成员变量char _name[20];int _age;public://成员方法void Init(const char* name, int age)   //初始化成员变量函数{strcpy(_name, name);_age = age;}void Print()  //打印成员变量函数{cout << "姓名:" << _name << endl;cout << "年龄:" << _age << endl;}
};int main()
{stu s1;s1.Init("zhangsan", 20);s1.Print();  return 0;
}
图3.3  演示代码3.2运行结果

关于访问限定符的几点说明:

  1. private修饰的成员可以在类外面直接被访问,public和protect修饰的成员不能在类外面被直接访问。
  2. 一个访问限定符限定的访问权限从这个访问限定符开始,到下一个访问限定符出现结束。如果该访问限定符后面没有访问限定符,则限定区域为该访问限定符出现到右括号 } 。
  3. class的默认权限为私有private,struct的访问权限默认为公有public。
  4. 当使用默认访问权限时,不建议省去访问限定符,也就是说,class中要通过private来显示的说明成员为私有类型,struct中也要显示的说明成员为公有,这样能增强代码的可读性。 

3.2 封装

面向对象的三大特性:封装、继承、多态。

在类和对象阶段,主要研究封装特性。所谓封装,就是将数据和方法进行有机结合,对外隐藏属性和实现细节,仅通过对外提供接口来实现与对象的交互。

封装是一种更为严格的管理,C语言不支持封装,所有可以认为C++相对于C语言更加严谨。通过演示代码3.3,可以说明封装管理的价值。演示代码3.3定义了一个栈类,名称设为Stack,其中包含三个成员变量:int* _a -- 指向存储数据的内存空间、int _size -- 栈中现有数据个数、int _capacity -- 栈容量,同时还有三个成员函数:Init -- 栈初始化、Push -- 压栈、Print -- 打印栈中数据、Destroy -- 栈销毁。

将成员变量设为private属性,将成员函数设为public属性,这些,成员函数就充当了对外接口,来实现与类stack的交互,不能在类的外部随意对成员变量进行操控。如果采用C语言实现栈,由于无法实现封装,那么在主函数中就可以单独操控某个成员变量,造成不可预料的后果。

演示代码3.3:

class Stack
{
public:void Init(int capacity = 4)  //栈初始化函数{_a = (int*)malloc(capacity * sizeof(int));if (_a == NULL){printf("malloc fail\\n");exit(-1);}_size = 0;_capacity = capacity;}void Push(int x)  //压栈函数{if (_size == _capacity){//检查栈容量是否足够,不够则重新开辟空间(省略)}_a[_size] = x;_size++;}void Print()  //打印栈数据函数{for (int i = 0; i < _size; ++i){printf("%d ", _a[i]);}printf("\\n");}void Destroy()  //栈销毁函数{free(_a);_a = NULL;_size = _capacity = 0;}private:int* _a;int _size;int _capacity;
};int main()
{Stack s;s.Init();   //初始化栈s.Push(1);  //压栈s.Print();  //打印栈s.Push(2);  //压栈s.Print();  //打印栈s.Push(3);  //压栈s.Print();  //打印栈s.Destroy(); //销毁栈//s._size = 5;  //企图直接更改类中成员变量的值,报错return 0;
}
图3.4  演示代码3.3运行结果

四. 类的作用域

类会定义一个新的作用域,如果需要在类的外面定义类成员,就需要使用作用域限定操作符::。如演示代码4.1所示,在head.h头文件中声明一个名为stu的类,其中定义了两个成员变量_name和_age,声明了两个成员函数Init和Print。如果想要在stu.cpp源文件中定义Print函数,就需要使用::操作符。

演示代码4.1:

//head.h
#pragma once
#include<iostream>
#include<string.h>
using namespace std;class stu
{
private:char _name[20];int _age;public:void Print();void Init(const char* name, int age);
};//stu.cpp
#include "head.h"
void stu::Print()
{cout << "name:" << _name << endl;cout << "age:" << _age << endl;
}void stu::Init(const char* name, int age)
{strcpy(_name, name);_age = age;
}//test.cpp
#include "head.h"
int main()
{stu s;s.Init("lisi", 20);s.Print();return 0;
}
图4.1  演示代码4.1的运行结果

五. 类对象的存储方式和大小计算

5.1 类对象在内存中的存储方式

我们猜测,类对象有两种可能的存储方式:

  1. 每个对象中保存一份成员变量、保存一份成员函数地址,成员函数代码只保存一份。
  2. 对象中只保存成员函数,代码存储在公共代码区。

为了验证上面两种可能的存储方式那种正确,我们编写了演示代码5.1,在class定义的类c中,包含三个int型成员变量和两个成员函数,在主函数中通过sizeof计算类的大小并打印。结果表明,类c的大小和类实例化出来的对象c1的大小均为12bytes,如果方法1正确,那么类的和类实例化出来的对象的大小都应该是20bytes,可见,2才是类对象的正确存储方式。

演示代码5.1:

class C
{//成员变量
private:int _i1;int _i2;int _i3;//成员函数
public:void func1() { };void func2() { };
};int main()
{C c1;cout << "sizeof(C) = " << sizeof(C) << endl;  //类的大小 -- 12cout << "sizeof(c1) = " << sizeof(c1) << endl;  //类对象的大小 -- 12return 0;
}
图5.1  演示代码5.1的运行结果
图5.2  类对象在内存中的存储

类对象的成员,在内存中的存储规则与C语言中结构体内存对齐规则一致,C语言结构体的内存对齐规则为:

  1. 结构体首个成员存储在偏移量为0的位置。
  2. 其他成员变量要对齐到这个成员变量对齐数的整数倍处。对齐数为默认对齐数和该成员变量的类型大小中较小的那个。在VS编译环境下默认对齐数的大小为8。
  3. 结构体的总大小需要为最大对齐数的整数倍。

5.2 空类的大小

结论:空类的大小为1bytes。

演示代码5.2:

class c1
{
public:void func1() {};
};class c2 {};int main()
{cout << "sizeof(c1) = " << sizeof(c1) << endl;cout << "sizeof(c2) = " << sizeof(c2) << endl;return 0;
}
图5.3 演示代码5.2的运行结果

为什么空类中什么都没有,还要占1bytes的内存空间呢?这1bytes的内存空间要起到一个占位作用,因为如果空类的大小为0,那么如果用这个类初始化出来两个对象,那么就会存在如何区分这两个类对象的问题,如果对两个对象取地址得到的都是nullptr,那么就无法对类对象进行区分。为空类分配的这1bytes的内存空间不存储任何有效数据。

六. this指针

6.1 什么是this指针

在上一章中讲到,同一个类的所有成员函数都只在公共代码区域存储一份,那么,在调用类成员函数操控成员变量是,怎样确定是操控哪一个类对象的成员变量呢?

这里就涉及到类成员函数隐藏的一个形参:this指针。演示代码6.1定义了一个日期类Date,在主函数中创建了两个类对象d1和d2。通过调用Init函数和Print函数,分别打印类成员信息。我们可以看到,d1和d2成员变量的值被正确区分,因为,Init函数和Print函数都存在一个隐藏的形参:Data* this。从表面上看,Init函数有三个参数,Print函数没有参数。但实际上,在调用函数时,都将指向类对象的this指针作为参数传给了成员函数。

演示代码6.1:

class Date
{
private:int _year;int _month;int _day;public://本质上为:void Init(Date* this, int year, int month, int day)void Init(int year, int month, int day){_year = year;_month = month;_day = day;}//本质上为:void Print(Date* this)void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
};int main()
{Date d1;Date d2;d1.Init(2023, 2, 24);  //d1.Init(&d1, 2023, 2, 24)d1.Print();  //d2.Print(&d1)d2.Init(2023, 2, 25);  //d2.Init(&d2, 2023, 2, 25)d2.Print();  //d2.Print(&d2)return 0;
}
图6.1  演示代码6.1的运行结果

6.2 this指针的特性和使用规则

  1. this指针的参数类型为:类名* const this,在类成员函数内部,不能通过解引用this指针操控成员变量。
  2. 在声明和定义成员函数时,不能显示的将this指针作为函数形参。如:void Print(Date* this)是不被允许的。
  3. 在调用成员函数时,不能将this指针类型的数据作为形参显示的传递给成员函数。如:d1.Print(&d1)这样的调用方式是不被允许的。
  4. 在成员函数内部,可以显示的使用this指针。

在演示代码6.2中,调用成员函数Print,其中:cout << _year << endl 和 cout << this->_year << endl的意义是完全一致的。

演示代码6.2:

class Date
{
private:int _year;int _month;int _day;public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << endl;cout << this->_year << endl;}
};int main()
{Date d1;d1.Init(2023, 2, 24);d1.Print();return 0;
}
图6.2  演示代码6.2的运行结果

6.3 this指针的存储位置

一般情况下,this指针作为形式参数,会存储在栈中。但是,由于this指针在成员函数被调用阶段会被频繁大量的使用,所以,一些编译器会对this指针的存储位置进行优化:将this指针存储在寄存器中,以此来提高读取和访问数据的效率。

  • 数据读取速度:寄存器 > 高速缓存 > 内存
  • 空间大小:寄存器 < 高速缓存 < 内存
  • 制造成本:寄存器 > 高速缓存 > 内存

在VS2019编译环境中对演示代码6.2进行调试,打开汇编代码进行观察(如图6.3),可以看到,在调用函数之前,将类对象d1的地址载入到ecx寄存器中(汇编指令lea的意思为加载有效地址 -- load effective address),这就证实了VS2019编译器将this指针存储到了寄存器中。

图6.3  演示代码6.2的汇编代码