C++之二:类和对象
相关代码:
C语言是面向过程的,关注的是过程,分析求解问题的步骤,调用函数逐步解决问题。
C++是面向对象的,关注的是对象,将一件事情的完成分为不同的几个参与者(对象),靠参与者的交互来完成任务。
一、类
C语言中,结构体只能用来定义变量;在C++中,结构体不仅可以定义变量,还可以定义函数。
C++中更常用class来定义类。
类体中内容称为类的成员:
- 类中的变量称为类的属性或成员变量。
- 类中的函数称为类的方法或者成员函数。
二、类的定义
类的两种定义方式:
1、声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
2、 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
3、定义一个类
// 定义一个数据结构栈的类
const int N = 10;
class Stack {
public:void Init() {_a = (int*)malloc(sizeof(int) * N);_top = 0;_capacity = N;}void Destory() {free(_a);_a = NULL;_top = -1;_capacity = 0;}void Push(int x) {if (_top == _capacity) {_capacity *= 2;int* tmp = (int*)realloc(_a, sizeof(int) * _capacity);if (tmp != nullptr) {_a = tmp;}}_a[++_top] = x;}void Pop() { _top--; }int Top() { return _a[_top]; }private:int* _a;int _top;int _capacity;
};
三、类的实例化
1、访问限定符
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
- public修饰的成员在类外可以直接被访问。
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)。
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。
- 如果后面没有访问限定符,作用域就到 } 即类结束。
- class的默认访问权限为private,struct为public(因为struct要兼容C)。
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
Q:C++中struct和class的区别是什么?
A:C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。(C++的struct中也可以定义函数,C中的struct不能定义函数。C++中class和struct唯一的区别就是默认权限的不同)。
2、封装
面向对象的三大特性:封装、继承、多态。
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
3、类实例化对象
类实例化对象,相当于定义出了类的成员变量。
(类,本质上就是一种自定义类型以及相应的操作函数)。
int main()
{//类实例化对象Stack st;st.Init();st.Push(1);cout << st.Top() << endl;st.Pop();st.Destory();return 0;
}
4、类的大小
与C语言结构体类似,存在内存对齐(不了解的可看结构体那一章,内存对齐的规则)。但是类实例化的对象的大小只与成员变量有关,不存放函数。
cout << sizeof(st) << endl; //12
上述Stack类对象的大小占12Byte,这就说明,一个类实例化出的对象并没有存储成员函数。
原因:
1、一个类实例化出N个对象,每个对象的成员变量存储的值可以是不同的。
2、但是调用的函数是相同的代码。
3、如果每个对象都存放成员函数,而这些成员函数是相同的。那么创建大量对象时,会浪费大量的空间。
4、因此:对象中只存放成员变量,而成员函数存放在公共代码段。
没有成员变量的类大小是1Byte。
class A2
{
public:void A_2(){}
};
class A3
{
};
Q:问什么没有成员变量的类的大小是1而不是0?
A:假设没有成员变量的类的大小是0。则当该类实例化一个对象,调用成员函数时,找不到实例化对象的地址,无法调用函数。因此,系统规定,没有成员变量的类的大小是1。
5、this指针
隐含的this指针:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数。
- 编译器让该指针指向当前对象,在函数体中所有“成员变量”的操作,都是通过该指针去访问。
- 只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
- this指针一定加载函数参数的第一个位置。(用户加不加都可以)。
怎么证明this指针确实存在?
对于一个成员函数,在该函数内部,当我们调用相应的成员变量时,我们没有传入相应参数,而且成员函数与成员变量没有存放在同一区域,那我们为什么可以调用该成员变量呢?
有关this指针的一个问题:
//下面的代码可以正常运行吗?
class A4
{
public:void Print(){cout << /*this->*/_a << endl;//当传入this==nullptr时,产生访问冲突}void Show(){cout << "show()" << endl;}
private:int _a;
};int main()
{A4* p = nullptr;p->Show(); //正常运行p->Print();//编译能通过,但是程序崩溃,原因:访问空指针
}
// Show(A4* this);Print(A4* this)
// p->Show() --> Show(p);
//p->Print() --> Print(p);要访问空指针,
四、类的默认函数
默认成员函数:当用户没有实现时,编译器会自动生成的。(包括默认构造函数、默认析构函数)
这六个函数之间的关系:
1、构造函数
构造函数,在创建对象时调用的函数,完成初始化工作。(初始化对象)。
- 函数名与类名相同。
- 无返回值。(返回void也不行)。
- 对象实例化时编译器自动调用对应的构造函数。(实例化时自动调用)。
- 构造函数可以重载。
- 如果没有构造函数,编译器会自动生成一个无参的默认构造函数。(显式定义后编译器不在生成)。
- 编译器自动生成的只会初始化自定义类型成员变量。(C++11在这里又有所修改)
- 无参的构造函数与全缺省的构造函数都称为默认构造函数,而默认构造函数只能有一个。(全缺省构造函数和无参构造函数不能同时存在)。
class Time
{
public:Time(){_hour = 0;_minute = 0;_second = 0;}
private:int _hour;int _minute;int _second;
};class Date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;Time _t;// 当Date没有构造函数时,会对Time调用构造函数初始化,对int型不初始化
};int main()
{Date d1;//调用编译器生成的构造函数d1.Print();return 0;
}
//我们没有显式定义构造函数,这里编译器会生成无参的默认构造函数//默认的构造函数:// 1、针对内置类型的成员变量没有初始化// 2、对自定义类型的成员变量,调用它的构造函数初始化 //构造函数,在对象构造时调用的函数,完成初始化工作/*Date(){//全缺省的构造函数和无参默认构造函数不能同时存在:存在歧义_year = 0;_month = 0;_day = 0;}Date(int year, int month, int day){_year = year;_month = month;_day = day;}*/Date(int year = 0, int month = 0, int day = 0)//更好的书写构造函数方式 --> 全缺省{_year = year;_month = month;_day = day;}
默认构造函数有三个:无参构造函数、全缺省的构造函数、编译器自动生成的。
三者共同的特点是:不需要传入参数就可以初始化。
2、析构函数
析构函数:对象生命周期到了以后,自动调用。
完成对象里面的资源清理工作,不是对d1和d2的销毁。
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
- 对于没有开辟动态内存的类,析构函数写不写都可以。
- 与构造函数类似,当使用编译器自动生成的析构函数时,只会析构自定义成员变量。
Q:为什么说析构函数是资源清理而不是销毁对象?
A:当成员变量中存在动态开辟的内存时,调用析构函数会释放这片空间,但是对象仍然存在,析构函数调用完后,对象的生命周期走到尽头,会被系统自动销毁。
//析构函数:对象声明周期到了以后,自动调用。//完成对象里面的资源清理工作,不是对d1和d2的销毁~Date(){//没有参数也没有返回值cout << "~Date()" << endl;}
3、拷贝构造函数
拷贝构造函数:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类实例化对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
了解引用之后,要记得C++对于传参有三种形式:
1、传值,(最浪费空间,而且不能修改原值)。
2、传址,(占用的空间与CPU位数有关,解引用修改原值)。
3、传引用,(不开辟空间,对引用的操作直接作用在原值)。
//const保证不通过d来修改d1
Date(const Date& d)
{_year = d._year;_month = d._month;_day = d._day;
}
Date d1(17,5,6);
Date d2(d1); //等价于d2 = d1 ,即拷贝构造函数
Date d3 = d1;
4、运算符重载函数
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数。
也具有返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名:关键字operator后面接需要重载的运算符符号。
- 不能通过operator创建新的运算符,只能重载存在的运算符。
- 重载运算符必须有一个自定义类型的操作数。(否则会和底层库冲突)。
- 有五个运算符不能重载:( .* :: sizeof ?: . )。
- 作为类成员函数重载时,形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this。
1、当operator重载运算符函数做成员变量时,有一个隐含的this指针。
2、 当operator重载运算符函数做普通函数时,要求类的成员变量必须是公有的。
实现:
// 运算符有几个操作数,operator就有几个参数(带this指针)
bool operator==(const Date& d) {return d._year == _year && d._month == _month && d._day == _day;
}
bool operator>(const Date& d) {if (this->_year > d._year)return true;else if (this->_year == d._year && _month > d._month)return true;else if (this->_year == d._year && _month == d._month && _day > d._day)return true;elsereturn false;
}
5、赋值运算符重载
五、实现一个类
#include <iostream>
using namespace std;
//要求:实现一个日期类,完成日期的相加减、比较等
class Date {
public:int GetMonthDay(int year, int month) {static int monthDays[13] = {0, 31, 28, 31, 30, 31, 30,31, 31, 30, 31, 30, 31};// 是2月且是闰年返回29if (month == 2 && (year % 4 == 0 && year % 100 != 0) ||year % 400 == 0) {return 29;}return monthDays[month];}Date(int year = 0, int month = 1, int day = 1) {if (year >= 0 && month >= 1 && month <= 12 && day >= 1 &&day <= GetMonthDay(year, month)) {_year = year;_month = month;_day = day;} else {cout << "非法日期" << endl;_year = 0;_month = 1;_day = 1;}}Date(const Date& d) {_year = d._year;_month = d._month;_day = d._day;}// 运算符重载bool operator==(const Date& d) {return _year == d._year && _month == d._month && _day == d._day;}bool operator<(const Date& d) {if (_year < d._year)return true;else if (_year == d._year && _month < d._month)return true;else if (_year == d._year && _month == d._month && _day < d._day)return true;elsereturn false;}// d1<=d2// d1.operator<=(&d1,d2)// bool operator<=(Date* this,const Date& d)bool operator<=(const Date& d) {return *this < d ||*this ==d; // 复用上面的代码,this是 Date对象的指针,*this就是该对象}bool operator>(const Date& d) {return !(*this <= d); // 复用上面的代码,}bool operator>=(const Date& d) {return *this > d || *this == d; // 复用上面的代码,}bool operator!=(const Date& d) {return !(*this == d); // 复用上面的代码}Date operator+(int day) {Date ret = *this; // 拷贝构造一个retret._day += day;while (ret._day > GetMonthDay(ret._year, ret._month)) {// 如果天数不合法,往月进位ret._day -= GetMonthDay(ret._year, ret._month);ret._month++;if (ret._month == 13) {ret._year++;ret._month = 1;}}return ret;}Date& operator+=(int day) {_day += day;while (_day > GetMonthDay(_year, _month)) {_day -= GetMonthDay(_year, _month);_month++;if (_month == 13) {_year++;_month = 1;}}return *this;}Date operator-(int day) {Date ret = *this;ret._day -= day;while (ret._day <= 0) {ret._month--;if (ret._month == 0) {ret._year--;ret._month = 12;}ret._day += GetMonthDay(ret._year, ret._month);}return ret;}Date& operator-=(int day) {_day -= day;while (_day <= 0) {_month--;if (_month == 0) {_year--;_month = 12;}_day += GetMonthDay(_year, _month);}return *this;}int operator-(const Date& d) {}void Print() { cout << _year << "-" << _month << "-" << _day << endl; }private:int _year;int _month;int _day;/*复用的好处:当加入其他成员变量时,只需要修改一部分代码*/// 复用是内聚性高:// 类间提倡:// 类内提倡:低耦合高内聚/*int _hour;int _minute;int _second;*/
};int main() {Date d1(2021, 1, 7);d1.Print();Date d2(2021, 2, 23);d2.Print();cout << (d1 == d2) << endl;cout << (d1 <= d2) << endl;cout << (d1 >= d2) << endl;cout << (d1 != d2) << endl;cout << (d1 < d2) << endl;cout << (d1 > d2) << endl;// 是否重载运符,是看这个运算符对这个类是否有意义d1 = d1 + 1000; //+天数d1.Print();d2 += 10;d2.Print();d1 = d1 - 1000; //-天数d1.Print();d1 -= 10;d1.Print();// d1 - d2;//两日期相差
}