【C++】类和对象(中)
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、pandas是什么?
- 二、使用步骤
- 1.引入库
- 2.读入数据
- 总结
前言
我们前面了解了类的基本知识,其中提到了成员函数,现在我们需要具体了解类中默认成员函数。
一、类的6个默认成员函数
如果一个类中一个成员都没有,简称为空类。
那么空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下六个默认成员函数。
默认成员函数:用户没有显示实现,编译器会生成的成员函数称为默认成员函数。
二、构造函数
2.1 概念
对于下面这个Date类:
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(2022, 7, 5);d1.Print();Date d2;d2.Init(2022, 7, 6);d2.Print();return 0;
}
我们可以通过Init函数去给对象设置日期,但如果每次创建对象时都调用该方法设置日期,未免有点太麻烦了,那能否在对象创建时,就将信息设置进去呢?构造函数就是解决这个问题的。
构造函数是一个特殊的成员函数:名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
2.2 特性
构造函数是特殊的成员函数,需要注意的是,构造函数名称虽然叫做构造,但是构造函数的主要任务并不是开辟空间创建对象,而是初始化对象。
其特性如下:
- 函数名和类名相同
- 无返回值(连void也没有,返回值处无用写任何东西)
- 对象实例化时编译器自动调用对应的构造函数
- 构造函数可以重载
class Date
{
public:// 构造函数只有函数名,参数,函数体,没有返回值// 无参的构造函数Date(){}// 有参数的构造函数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; // 调用无参的构造函数Date d2(2025, 7, 24); // 调用有参数的构造函数// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明// 下面这个代码,声明了d3函数,该函数无参,返回一个Date类型的对象Date d3();return 0;
}
5. 如果类中没有显示定义构造函数,则C++编译器会自动成成一个无参的默认构造函数,一旦用户显示定义编译器就不再生成。
class Date
{
public:// 这里显示生成了含参的构造函数,编译器不再生成默认构造函数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()
{// 编译器没有生成默认构造函数,也就没有无参的构造函数,d1这里就会报错Date d1; Date d2(2025, 7, 24); return 0;
}
6. C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int / char...,自定义类型就是我们使用class / struct / union等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。
class Time
{
public:Time(){cout << "Time()" << endl;_hour = 0;_minute = 0;_second = 0;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本类型(内置类型)int _year;int _month;int _day;// 自定义类型Time _t;
};
int main()
{Date d;return 0;
}
注意:C++11中针对内置类型成员不初始化的缺陷,打了个补丁,即:内置类型成员变量在类中声明时可以给默认值。
class Date
{
private:// 基本类型(内置类型)int _year = 2025;int _month = 7;int _day = 34;// 自定义类型Time _t;
};
7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数,全缺省构造函数和我们没写有编译器默认生成的构造函数,都可以认为是默认构造函数。
class Time
{
public:// 无参构造函数Time(){cout << "Time()" << endl;_hour = 0;_minute = 0;_second = 0;}// 全缺省构造函数Time(int hour = 19, int minute = 44, int second = 33){_hour = hour;_minute = minute;_second = second;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本类型(内置类型)int _year = 2025;int _month = 7;int _day = 34;// 自定义类型Time _t;
};
int main()
{Date d;return 0;
}
三、析构函数
3.1 概念
通过前面构造函数的学习,我们知道了一个对象是怎么来的,那一个对象又是怎么没的呢?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象销毁时会自动调用析构函数,完成对象中资源的清理工作。
3.2 特性
析构函数也是特殊的成员函数,其特性如下:
1. 析构函数名是在类名前面加上符号~
2. 无参数无返回值类型
3. 一个类只能有一个析构函数。若未显示定义,编译器会自动生成默认的析构函数。注意:析构函数不能重载。
4. 对象生命周期结束时,C++编译器自动调用析构函数
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 3){_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("malloc申请空间失败!!!");return;}_capacity = capacity;_size = 0;}void Push(DataType data){// CheckCapacity();_array[_size] = data;_size++;}// 其他方法...~Stack(){cout << "~Stack()" << endl;if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:DataType* _array;int _capacity;int _size;
};
int main()
{Stack s;s.Push(1);s.Push(2);return 0;
}
5. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定义类型成员调用它的析构函数。
class Time
{
public:// 全缺省构造函数Time(int hour = 19, int minute = 44, int second = 33){_hour = hour;_minute = minute;_second = second;}~Time() {cout << "~Time()" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本类型(内置类型)int _year = 2025;int _month = 7;int _day = 34;// 自定义类型Time _t;
};
int main()
{// Date生成默认的析构函数,这个析构函数调用了Time的析构函数Date d;return 0;
}
6. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时一定要写,否则会造成资源泄漏,比如Stack类。
四、拷贝构造函数
4.1 概念
在现实生活,会存在两个人十分相似,我们一般称其为双胞胎。
那么在创建对象时,可否创建一个与已存在的对象一模一样的新对象呢?
拷贝构造函数:只有一个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时有编译器自动调用。
4.2 特性
其特性如下:
1. 拷贝构造函数是构造函数的一种重载形式
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用
class Date
{
public:Date(int year = 2025, int month = 7, int day = 24){_year = year;_month = month;_day = day;}// Date(const Date d) // 错误写法:编译报错,会引发无穷递归Date(const Date& d) // 正确写法{_year = d._year;_month = d._month;_day = d._day;}
private:// 基本类型(内置类型)int _year;int _month;int _day;// 自定义类型Time _t;
};
3. 若未显示定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,也叫做值拷贝
class Time
{
public:// 全缺省构造函数Time(int hour = 19, int minute = 44, int second = 33){_hour = hour;_minute = minute;_second = second;}Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;cout << "Time(const Time& t)" << endl;}~Time() {cout << "~Time()" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
public:Date(int year = 2025, int month = 7, int day = 24){_year = year;_month = month;_day = day;}
private:// 基本类型(内置类型)int _year;int _month;int _day;// 自定义类型Time _t;
};
int main()
{Date d1;// Date没有显示定义拷贝构造函数,编译器自动生成默认拷贝构造函数// 默认拷贝构造函数对于自定义类型会去调用它的拷贝构造函数Date d2(d1);return 0;
}
4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显示实现吗?当然,对于日期类的不需要显示实现,但是下面这个呢?
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 3){_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("malloc申请空间失败!!!");return;}_capacity = capacity;_size = 0;}void Push(DataType data){// CheckCapacity();_array[_size] = data;_size++;}// 其他方法...~Stack(){cout << "~Stack()" << endl;if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:DataType* _array;int _capacity;int _size;
};
int main()
{Stack s;s.Push(1);s.Push(2);Stack s1(s);// 这里我们想要s1后面有数字3,s后面只有数字1,2// 但实际会是这样吗s1.Push(3);return 0;
}
我们能看到s和s1的_array都指向了同一块空间,所以只要发生数据修改就会影响到其他对象,这不符合我们的需求。
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
五、赋值运算符重载
5.1 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符,比如:operator#
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的指针this
- .* :: sizeof ?: . 注意以上5个运算符不能重载
class Date
{
public:Date(int year = 2025, int month = 7, int day = 24){_year = year;_month = month;_day = day;}bool operator==(const Date& d){if (_year == d._year && _month == d._month && _day == d._day)return true;else return false;}
private:// 基本类型(内置类型)int _year;int _month;int _day;// 自定义类型Time _t;
};
int main()
{Date d1;Date d2(d1);cout << d1.operator==(d2) << endl;return 0;
}
5.2 赋值运算符重载
1. 赋值运算符重载格式
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 参数类型:const T&,传递引用可以提高传递效率
- 检测是否自己给自己赋值
- 返回*this:要符合连续赋值的含义
class Date
{
public:Date(int year = 2025, int month = 7, int day = 24){_year = year;_month = month;_day = day;}bool operator==(const Date& d){if (_year == d._year && _month == d._month && _day == d._day)return true;else return false;}Date& operator=(const Date& d){if (this != &d) {_year = d._year;_month = d._month;_day = d._day;}return *this;}
private:// 基本类型(内置类型)int _year;int _month;int _day;// 自定义类型Time _t;
};
2. 赋值运算符只能重载成类的成员函数,不能重载成全局函数
// 定义为全局函数,没有了this指针,需要给两个参数
Date& operator=(Date& d1, Date& d2)
{if (&d1 != &d2) {d1._year = d2._year;d1._month = d2._month;d1._day = d2._day;}return d1;
}
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
3. 用户没有显示实现时,编译器会生成一个默认的,以值得方式逐字节拷贝。 注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
class Time
{
public:// 全缺省构造函数Time(int hour = 19, int minute = 44, int second = 33){_hour = hour;_minute = minute;_second = second;}Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;cout << "Time(const Time& t)" << endl;}Time& operator=(const Time& t){cout << "Time& operator=(const Time& t)" << endl;if (this != &t) {_hour = t._hour;_minute = t._minute;_second = t._second;}return *this;}~Time() {cout << "~Time()" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
public:Date(int year = 2025, int month = 7, int day = 24){_year = year;_month = month;_day = day;}bool operator==(const Date& d){if (_year == d._year && _month == d._month && _day == d._day)return true;else return false;}
//private:// 基本类型(内置类型)int _year;int _month;int _day;// 自定义类型Time _t;
};
int main()
{Date d1;Date d2;d2 = d1;return 0;
}
默认的赋值运算符重载也是逐字节,跟拷贝构造函数一样,要注意的也一样。
注意:类中如果没有涉及资源申请时,赋值运算符重载是否写都可以;一旦涉及到资源申请时,则赋值运算符重载是一定要写的,否则就是浅拷贝。
5.3 前置++和后置++重载
class Date
{
public:Date(int year = 2025, int month = 7, int day = 24){_year = year;_month = month;_day = day;}bool operator==(const Date& d){if (_year == d._year && _month == d._month && _day == d._day)return true;else return false;}// 前置++// this指针指向的对象不会销毁,这里用引用增加效率Date& operator++(){_day++;return *this;}// 后置++// 前置++和后置++都是一元运算符,为了能让这两个正确的重载// C++规定:后置++重载多增加一个int参数,但调用函数时该参数不用传递,编译器自动传递// 后置++是先使用再+1,所以需要传递旧值,这就需要先保存旧值,然后再+1,这里就不能直接返回*thisDate& operator++(int){Date tmp(*this);_day++;return tmp;}
//private:// 基本类型(内置类型)int _year;int _month;int _day;// 自定义类型Time _t;
};
int main()
{Date d;Date d1(2025, 7, 24);d = d1++; // d: 2025,7,24 d1:2025,7,25d = ++d1; // d: 2025,7,26 d1:2025,7,26return 0;
}
六、const成员函数
const修饰的“成员函数”称为const成员函数, const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
我们来看看这个代码:
class Date
{
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << "Print()" << endl;cout << "year:" << _year << endl;cout << "month:" << _month << endl;cout << "day:" << _day << endl << endl;}void ConstPrint() const{cout << "Print()const" << endl;cout << "year:" << _year << endl;cout << "month:" << _month << endl;cout << "day:" << _day << endl << endl;}void test(){ConstPrint();}void consttest() const{Print();}
private:int _year; // 年int _month; // 月int _day; // 日
};
int main()
{Date d1(2025, 7, 24);d1.ConstPrint();const Date d2(2025, 7, 24);d2.Print();
}
这里有几个问题:
- const对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数内可以调用其他非const成员函数吗?
- 非const成员函数内可以调用其他const成员函数吗?
1和3是可以的,2和4不可以。这就跟const变量可以被非const变量赋值,但非const变量不能被const变量赋值一样。
七、取地址及const取地址操作符重载
class Date
{
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}Date* operator&(){return this;}const Date* operator&() const{return this;}
private:int _year; // 年int _month; // 月int _day; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况才需要重载,比如想让别人获取到指定的内容。
总结
这篇文章我们了解了六大默认成员函数中最常用的4个:构造函数,析构函数,拷贝构造函数,赋值运算符重载,并了解了const在类中的一些用处,希望对大家有所帮助。