类和对象(上)
前言
C语言是面向过程的,关注的是过程,C++是面向对象的,关注的是对象。
对于洗衣服这件事情,总共有四个对象,人,洗衣粉,洗衣机,衣服。
我们在洗衣服的时候,会先把洗衣机打开,然后把衣服放进去,然后放洗衣粉进去,开启洗衣机,等到洗衣机洗干净之后也会自己进行甩干。这是洗衣服的过程。这个过程是通过四个对象交互进行完成。
一、C++中的struct
我们之前在C语言中学习的 struct 只能够定义变量,在C++中不仅可以定义变量,还可以定义函数。因此C++中 struct 也可以定义类。
typedef int DataType;
struct Stack
{
void Init(size_t capacity)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_capacity = capacity;
_size = 0;
}
DataType* _array;
size_t _capacity;
size_t _size;
};
int main()
{
Stack s;
s.Init(5);
return 0;
}
在C++中,我们更喜欢用class来定义一个类。
二、类的定义
class Person
{
public: //权限
void Print() //成员函数
{
cout >> _name >> "" >> _age >> endl;
}
public:
string _name; //成员变量
int _age;
}; //分号不能省略
class为定义类的关键字,Person为类的名字,{}中为类的主体,类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
我们来看看类的两种定义方式。
- 声明和定义全部放在类体中(上面即是)
- 类声明放在.h文件中,成员函数定义放在.cpp文件中
接下来看看成员变量命名
class Person
{
void Init(int age)
{
//age=age;
//这里是不是会分不清楚是形参还是成员变量啊
//这里我建议是成员变量加一个下划线
//可以按照自己的风格进行修改
_age=age;
}
//int age;
int _age;
};
三、类的访问限定符和封装
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
C++中有三种访问限定符:public(公有)、protected(保护)和private(私有)
关于访问限定符的一些说明:
- public修饰的成员在类外可以直接被访问
- protected和private修饰的成员在类外不能直接被访问
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到 } 即代表类结束
需要注意的一点就是class的默认访问权限为private,struct为public。
接下来看看封装
面向对象的三大特性:封装、继承、多态。
什么是封装呢 ?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装无处不在,就好比我们使用的电脑,我们现在使用的都是图形化界面,有图形有按键,方便了用户的使用和体验。我们点击一个按键打开了一个网页,我们用眼睛只是看到了页面的变化,而实际工作的是内部的各种硬件,对于我们用户,我们需要知道里面的结构吗,我们其实不需要,我们只需要体验就可以了,我们只需要知道怎么去使用即可,这就体现了一种封装的思想。
那么在C++中是如何实现封装的呢 ?
通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
我们知道作用域这一个概念,比如全局变量的作用域是全局,而局部变量的作用域是只在局部,那么类的作用域呢 ? 我们接下来看看类的作用域。
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 ::
作用域操作符指明成员属于哪个类域。
四、类的实例化
类创建对象的过程就叫做类的实例化。
class Person
{
void Print()
{
}
int _age;
}
Person p1;
Person P2;
此时还没有给类还不占空间,我们只是限定了类有哪些成员,但是并没有分配实际的内存空间来存储它。只有我们实例化出对象才真正的分配空间。一个类可以实例化出多个对象。
五、类对象模型
我们知道int 类型是4个字节(32位下),char 是1个字节,那么类的大小怎么计算呢 ?
#include <iostream>
using namespace std;
class Person
{
public:
void Print()
{
cout << _age << endl;
}
private:
int _age;
};
int main()
{
Person p1;
cout << sizeof(p1) << endl;
return 0;
}
这个的大小怎么算呢 ?我们知道int 是4个字节(32位下),那么函数的大小怎么计算呢 ?
结果是4个字节。(32位下)
结果只保存成员变量,成员函数存放在公共的代码段。那么为什么这么来设计呢 ?
我们来想一个问题:
每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么如何解决呢 ?
所以我们得到了成员函数存放在公共的代码段的结果。
#include <iostream>
using namespace std;
class Person
{
public:
void Print()
{
}
private:
};
int main()
{
Person p1;
cout << sizeof(p1) << endl;
return 0;
}
这次的结果又是什么呢 ?
结果怎么是1呢 ?
我们可以看到这个类里面只有一个成员函数,没有成员变量,这个时候可能会想函数是放在公共的代码段的,不占类的大小,所以占 0 个字节 ,可是结果却并不是这样的,这是因为我能实例化出对象,我就要让系统能够找到我这个对象,那我要是占 0 个字节,系统是不是根本找不到我啊,所以系统还是给了我 1 个字节,让我占个位置,方便以后能够找到我,所以这1 个字节是用来表示我这个对象的。
六、this指针
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;
};
int main()
{
Date d1, d2;
d1.Init(2024,10,10);
d2.Init(2024,10,11);
d1.Print();
d2.Print();
return 0;
}
这里是不是打印出来我们想要的结果了,可是函数体中没有关于不同对象的区分,那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
这个时候我们就引入了 this 指针了。
this指针指向调用函数的对象(需要注意我们不能手动显式地在参数列表中添加this指针,编译器自己会完成)
#include <iostream>
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print()
{
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1, d2;
d1.Init(2024, 10, 10);
d2.Init(2024, 10, 11);
d1.Print();
d2.Print();
return 0;
}
我们来看一下this指针的一些特性。
- this指针的类型:类类型const Type* const pointer,即成员函数中,不能给this指针赋值。
- 只能在“成员函数”的内部使用
- this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
- this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过 ecx 寄存器自动传递,不需要用户传递。
那么this指针可以为空吗 ?我们来看一下。
#include <iostream>
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print()
{
cout << "Print()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date* p1 = nullptr;
p1->Print();
return 0;
}
#include <iostream>
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print()
{
cout << "Print()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date* p1 = nullptr;
p1->Init(2024,10,10);
return 0;
}
我们打开调试窗口来看一下。
有时候打开调试窗口来看的话会更清楚一些。
这里的this 指针已经是 nullptr 了,此时解引用不相当于对空指针进行解引用了吗 ,此时还会有一个疑问,为什么第一个打印出来了,第一个也是this指针指向的啊,这是因为出现箭头并不代表着一定会解引用,Print 不在对象内部,也没有对空指针p1进行解引用,所以可以正常运行,而Init中进行了解引用(访问了成员变量),访问了空指针,所以崩溃。
补充前面的一些知识(用C++实现栈),可以跟前面的C语言实现栈进行一下对比。
typedef int DataType;
class Stack
{
public:
void Init()
{
_array = (DataType*)malloc(sizeof(DataType) * 3);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = 3;
_size = 0;
}
void Push(DataType data)
{
CheckCapacity();
_array[_size] = data;
_size++;
}
void Pop()
{
if (Empty())
return;
_size--;
}
DataType Top()
{
return _array[_size - 1];
}
int Empty()
{
return 0 == _size;
}
int Size()
{
return _size;
}
void Destroy()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
void CheckCapacity()
{
if (_size == _capacity)
{
int newcapacity = _capacity * 2;
DataType* temp = (DataType*)realloc(_array, newcapacity *sizeof(DataType));
if (temp == NULL)
{
perror("realloc申请空间失败!!!");
return;
}
_array = temp;
_capacity = newcapacity;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
七、类的6个默认成员函数
class Person
{
};
如果一个类中什么成员都没有,就称为空类,这个空类真的是空的吗 ?
答案是并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
#include <iostream>
using namespace std;
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;
};
int main()
{
Date d1;
d1.Init(2024, 10, 10);
d1.Print();
return 0;
}
1.构造函数
对于这个Date类,我们可以使用公有成员函数Init来初始化对象,但如果每次创建对象时都调用该方法设置
信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
答案是可以的,构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,使每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
构造函数的特征
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
#include <iostream>
using namespace std;
class Date
{
public:
Date()
{
_year = 2024;
_month = 10;
_day = 11;
}
Date(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;
};
int main()
{
Date d1;
d1.Print();
Date d2(2024, 10, 10);
d2.Print();
return 0;
}
一个无参一个有参,当我实例化对象的时候,如果不传参就调用无参的,如果传参就会调用有参的构造函数,(需要注意的是如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
用户显式定义编译器将不再生成)
#include <iostream>
using namespace std;
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
这里怎么生成的是随机值,好像看起来没有多大的用处啊,这是怎么回事呢 ?
因为C++把类型分为了内置类型(基本类型)和自定义类型。内置类型就是语言自己提供的例如int、char等类型,而自定义类型就是我们使用class/struct/union等自己定义的类型。这也可以看做是一个缺陷,但是C++11对这个缺陷做了处理,即内置类型的成员变量在声明时可以给默认值。
#include <iostream>
using namespace std;
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year = 2024;
int _month = 10;
int _day = 10;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
我们再来看看默认构造函数。
#include <iostream>
using namespace std;
class Date
{
public:
Date()
{
_year = 2024;
_month = 10;
_day = 11;
}
Date(int year = 2024, int month = 10, int day = 10)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print(); //会调用函数进行初始化?
return 0;
}
这里我们就可以这样认为,无参的构造函数和全缺省的构造函数也认为是默认构造函数,并且默认构造函数只能有一个。
2.析构函数
析构函数是用来回收资源的,我们以前用malloc的时候,到最后我们要进行手动的free,为了防止内存泄漏。
析构函数的特性:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
#include <iostream>
using namespace std;
class Date
{
public:
~Date()
{
cout << "~Date()" << endl;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year = 2024;
int _month = 10;
int _day = 10;
};
int main()
{
Date d1;
return 0;
}
我们再来看一种情况。
#include <iostream>
using namespace std;
class Date
{
public:
Date()
{
cout << "Date()" << endl;
}
~Date()
{
cout << "~Date()" << endl;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year = 2024;
int _month = 10;
int _day = 10;
};
class test
{
public:
test()
{
cout << "test()" << endl;
}
~test()
{
cout << "~test()" << endl;
}
private:
int _data;
Date date;
};
int main()
{
test d1;
return 0;
}
可以观察一下特点,是不是先构造的后析构了啊,是不是有点栈的特点了。因为在test类中有类型为Date的成员变量,在销毁对象d1时要先销毁Date类的成员变量date,再去调自身的。(没有进行资源申请时,可以不用显式定义析构函数)
3.拷贝构造函数
我们在创建对象时,可否创建一个与已存在对象一某一样的新对象呢 ?
答案是可以的。
拷贝构造函数的特性:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用。
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 2024, int month = 10, int day = 10)
{
_year = year;
_month = month;
_day = day;
}
//Date(const Date d) 错误写法,编译器直接报错
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year ;
int _month ;
int _day ;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
(需要注意没有进行资源申请时,可以不用显式定义析构函数)
4.赋值运算符重载
我们可以使用大于号、小于号等运算符来比较两个内置类型的变量,但是对于像日期类的对象而言,当我们想比较两个日期时,无法使用这些运算符来进行比较。所以会引入运算符重载。
函数原型:返回值类型 operator操作符(参数列表)
.* :: sizeof ?: . 注意以上5个运算符不能重载。
赋值运算符重载的特性:
- 我们只能在operator后加上已经存在的运算符进行重载,不能凭空创造一个新的操作符。
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 2024, int month = 10, int day = 10)
{
_year = year;
_month = month;
_day = day;
}
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;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year ;
int _month ;
int _day ;
};
int main()
{
Date d1;
Date d2(d1);
Date d3(2024, 10, 11);
cout << (d1 == d2) << endl; //因为流插入运算符的优先级较高,我们需要用括号把待比较的对象和运算符括起来
cout << (d1 == d3) << endl;
return 0;
}
这样是不是就完成了赋值运算符重载了。
赋值运算符重载格式
参数类型:const T&,传递引用可以提高传参效率。
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值。
检测是否自己给自己赋值。
返回*this :要复合连续赋值的含义。
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
前置++的重载
Date& operator++()
{
_day += 1;
return *this; //this指向的对象在函数结束后不会销毁,所以可以使用传引用返回提高效率
}
后置++的重载
Date operator++(int)
{
Date tmp(*this);
_day += 1;
return tmp;
}
因为后置++是先使用后+1,要返回未+1之前的旧值,所以需要tmp来保存*this的值,tmp出了作用域后就会销毁,所以只能传值返回。
后置++重载函数中的参数int没有实际作用,只是为了与前置++构成函数重载,以便进行区分。
以上的测试代码如下,如有需要的可以自取。
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 2024, int month = 10, int day = 10)
{
_year = year;
_month = month;
_day = day;
}
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;
}
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
Date& operator++()
{
_day += 1;
return *this;
}
Date operator++(int)
{
Date tmp(*this);
_day += 1;
return tmp;
}
~Date()
{
cout << "~Date()" << endl;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year ;
int _month ;
int _day ;
};
int main()
{
Date d1;
Date d2(d1);
Date d3(2024, 10, 11);
Date d4;
d4 = d1;
cout << (d1 == d2) << endl;
cout << (d1 == d3) << endl;
d4++;
return 0;
}
标签:对象,month,int,year,Date,day,d1
From: https://blog.csdn.net/2402_84602321/article/details/142827780