C语言中通过结构体可以自定义一个类型,但是功能较为狭隘,结构体中只能定义变量
而C++的类就是C语言结构体的一个增强,既可以定义变量又可以定义函数
类的定义
一个类可以实例化出不同的对象,每个对象都可以直接调用在类里面的成员。定义类需要用到class 或者 struct这两个关键字,class和struct又有一些区别,认识它们的区别前首先要了解类的访问限定符。
访问限定符
通过访问权限的选择可以控制用户对类里面成员的操作限制。
public – 公有
private – 私有
protected – 保护
使用访问限定符 – XXX: --在访问限定符的后面加上冒号,一个访问限定符在遇到下一个不同的访问限定符的整个区间里的成员都是属于该限定符的
以上三种就是C++类中的三种访问限定符,根据意思就可以大致理解其对应的含义。
class People { public: int a; int b; }; int main() { People man; man.a = 10; cout << man.a << endl; return 0; }
由于在类里面,a和b变量都是属于public的,因此类实例化出来的对象就可以直接调用。总结:公有的成员可以直接访问,私有和保护不能直接被调用
那么说回class 和 struct的区别,在定义一个类时可以不在类里面指明访问限定符,当用户不指明时 class默认所有成员都是私有,而struct默认为公有
类的实例化
类可以理解为是对对象的一个总体概括。当把人看作是一个类,那么这个类里面的成员就可以有名字,身份证号,性别,年龄等,那么每一个人都是一个对象,而每个人也就是每个对象对应的名字、身份证号等都是不一样的。这也就是面向对象的思想。
需要了解的是:
类本身定义出来时是没有实际的内存空间的,只有当实例化出对象的时候才会占用实际的物理空间
类对象的存储结构
因为一个类里面是会有变量和函数的,而类在实例化出来对象时就会占用内存空间。
事实上类实例化出来对象后,对象会自己拷贝一份类里面的成员变量,每一个对象都有一份属于自己的成员变量。但是对象不会去存储成员函数,因为函数是可以通过地址去找到并调用的,而每个对象调用一个函数的目的是相同的因此并不需要独立的函数。为了节省消耗,类的成员函数会放在一个公共的代码区,每个对象需要调用时只需要到这个公共的空间就可找到对应的函数
那么如果现在有一个空类实例化出来对象后会不会占用空间呢?
答案是需要的,当类想要实例化一个对象出来不管有没有数据都必须要占有空间,否则无法实例化。所以编译器会自动的给一些内存给空类的对象,至于给的是多少就要取决于编译器,VS下指定的是1个字节
那么如果一个类里面成员只有函数没有变量?
答案也是需要占用空间的,并且占用的空间和上述的情况一样,因为类的对象并没有存储成员函数,需要调用的时候只需要去公共的空间找就可以了。因此在VS下该类的对象同样也是1字节
而一个对象占用的空间多少取决于类的成员变量,其占用的空间也遵循C语言结构体里面的内存对齐规则
类的默认成员函数
其实一个空类并不是真正的空类,只是用户没有给类定义上成员而已。但是编译器会自动生成6个默认的成员函数
构造函数 – 负责初始化
析构函数 – 负责释放空间
拷贝构造 – 负责使用同类的已存在对象去初始化对象
赋值重载 – 目的等同拷贝构造
普通对象取地址 – 取地址
const对象取地址 – 去地址
这些默认的成员函数,如果用户不定义则编译器自动生成,如果用户定义了则使用用户定义的
构造函数
定义构造函数需要遵循名字和类名相同,每一次类实例化对象的时候编译器就会自动调用构造函数为对象初始化。需要注意的是构造函数如果设为私有那么编译器就会调用失败
#include<iostream> #include<string> #include<vector> using namespace std; class People { public: People() { name = "zhangsan"; age = 18; id = "20234567"; } private: string name; int age; string id; }; int main() { People man; return 0; }
可以看到当People类实例化出man对象后,会自动调用构造函数并根据函数内容初始化man对象的变量值。对于构造函数也可以使用初始化列表的方式编写,更加的简洁明了
class People { public: //初始化列表的由冒号开始,变量之间用逗号分开,每个变量后面不需要加上分号 //如果遇到一个表达式无法初始化可以在{}里面编写多行代码 People() :name("zhangsan") ,age(18) ,id("20234567") {} private: string name; int age; string id; };
构造函数的特性:
- 函数名与类名相同
- 没有返回值
- 编译器自动调用
- 构造函数可以构成重载,带参或者不带参都可以
- 如果用户没有定义构造函数,编译器会自动生成无参的构造函数
- 如果用户没有定义构造函数,并且在定义类成员变量时给上了初值,那么对象初始化的变量值默认为这个初值
- 一个类必须有且只有一个默认构造函数,无参的构造函数和全缺省构造函数都是默认构造函数
析构函数
与构造函数相反,析构函数是在对象销毁后,清理对象的资源。因为对象会存放着变量,而变量又会占用空间,而为了防止内存泄漏必须要在对象销毁时把对象的资源清理干净。
析构函数名是在类名前加上 ~ 符号
#include<iostream> #include<string> #include<vector> using namespace std; class People { public: People() { _start = new int[4]; } ~People() { delete[] _start; _start = nullptr; } private: int* _start; }; int main() { People man; return 0; }
在创建对象后,成员变量_start会根据构造函数的内容开辟出了空间。
当程序结束时也就是对象销毁时,会自动调用析构函数将_start申请的空间释放掉。这就是析构函数的作用所在
析构函数的特性:
- 析构函数名在类名前加上 ~
- 一个类有且仅有一个析构函数,其无参数无返回值不能重载
- 编译器在都西昂生命周期结束时自动调用
拷贝构造函数
拷贝构造函数是构造函数的一种,上面所说的默认构造函数是没有参数的情况的,而拷贝构造函数就是默认构造函数的重载
拷贝构造函数:
- 是默认构造函数的重载
- 有且仅有一个参数,并且参数不能使用传值方式传参
- 编译器自动调用
#include<iostream> #include<string> #include<vector> using namespace std; class People { public: //默认构造 People() {} //构造 People(string name,int age,string id) :_name(name) ,_age(age) ,_id(id) {} //拷贝构造 People(const People& p) :_name(p._name) ,_age(p._age) ,_id(p._id) {} private: string _name; int _age; string _id; }; int main() { //通过带参构造完成对象初始化 People man("zhangsan", 18, "20234567"); //通过拷贝构造初始化对象 People woman(man); return 0; }
当women对象创建好后其里面的成员变量值就会根据已存在的man对象的成员变量值初始化
那么问题来了,既然编译器都会默认生成拷贝构造函数了,那还要用户自己定义吗?
答案是根据情况而定。像一些普通的内置类型变量就可以不用写,但是如果变量涉及到了空间申请时就必须要写了。如果不写,拷贝构造后两个对象的变量就会指向同一块空间,那么这块空间的资源就会失控。涉及到空间申请时,拷贝构造函数就不能够直接的赋值,而是需要让新的对象的变量去新开辟一段空间再把已存在的那段空间里的数据拷贝到新的空间,这样才不会让两个变量指向同一块空间
赋值重载构造
其效果等同于拷贝构造函数,用= 直接创建出与已存在对象里变量相同的对象。而赋值重载的意思是将 = 重新定义成一个满足用户需求的运算符,其功能可由用户自行定义,其实在C++中不仅是=可以重载,基本上运算符都是可以重载的。
运算符重载
像内置类型的运算符语法层面都是可以实现的,但是例如一个日期类,那常规的+ - +=等运算符就不能直接满足需求了,因为日期的计算需要考虑到日的进位和月的进位。因为这种情况就需要重新定义运算符的功能,也就是运算符重载
运算符重载的关键字 – operator
class Date { public: Date(int day,int month,int year) :_day(day) ,_month(month) ,_year(year) {} Date(const Date& d) :_day(d._day) , _month(d._month) , _year(d._year) {} private: int _day; int _month; int _year; };
像这种类如果直接创建两个对象去比较是否相等的话,编译器根本就找不到有哪个运算符可以比较,因此就得用户自行去定义一个运算符进行比较。
class Date { public: Date(int day,int month,int year) :_day(day) ,_month(month) ,_year(year) {} Date(const Date& d) :_day(d._day) , _month(d._month) , _year(d._year) {} //重载== bool operator==(const Date& d) { return _day == d._day && _month == d._month && _year == d._year; } private: int _day; int _month; int _year; };
重载了==这个运算器,此时类的对象就可以进行比较了。其他的运算符重载也是这种概念。可以看到这个函数里面只有一个参数,但是却有两个对象的变量进行比较,那么函数没有传入当前的对象参数时是怎么找到当前对象的变量呢?其实函数里面是由两个参数的,在d对象之前还有一个参数this指针,只不过这个参数是可以隐藏的
this指针
this指针指向的是当前对象的地址,在对象调用成员函数时,this指针会把对象的地址作为实参传给函数。函数通过this指针的地址就可以直接找到对象。每一个类函数的内部都会隐藏了一个this指针参数
因此上述的 ==函数时,==前面的变量就是当前调用这个函数的对象的变量,只不过是this指针可以隐藏所以不写,也可以写上去
那么说回赋值构造,本质上就是重载 =运算符达到实例化对象可以使用 = 号构造的目的
class Date { public: Date(int day,int month,int year) :_day(day) ,_month(month) ,_year(year) {} Date(const Date& d) :_day(d._day) , _month(d._month) , _year(d._year) {} Date& operator=(const Date& d) { if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; } private: int _day; int _month; int _year; }; int main() { //通过带参构造完成对象初始化 Date d1(2023, 6, 19); //通过赋值构造初始化对象 Date d2 = d1; return 0; }
重载= 需要有返回值,而这个返回值就是当前对象。
友元函数
有一些函数如果在类里面去定义时可能会导致this指针不在函数的第一个参数,这种情况是不可以的this指针必须要在函数的第一个参数。因此想要定义这种函数的话就不可以在类里面定义,而是要放在类外面去定义,但是类外面又不能访问到类的成员变量,因此就有了友元函数这个概念。
友元函数可以访问类的所有成员包括私有,它是定义在类外部的普通函数,不需要某个类,但是如果想要访问到某个类的成员就需要在该类里面声明这个函数,并且在声明函数前加上关键字 friend
像最常见的 << >> 这两个流,如果想要重载就必须在类的外部