C++初阶教程——类与对象(中篇)
一、默认成员函数
在C++中,存在六个默认成员函数,如果这些函数没有被显式定义,那么编译器会自动生成这些函数。它们分别是默认构造函数、复制构造函数、复制赋值运算符、移动构造函数、移动赋值运算符和析构函数。
二、构造函数
2.1构造函数的概念
对于下面的类:
#include<iostream>
using namespace std;
class Date
{
private:
int _day;
int _month;
int _year;
public:
void Date_Init(int day, int month, int year)
{
this->_day = day;
this->_month = month;
this->_year = year;
}
void print()
{
cout << this->_year << "/" << this->_month << "/" << this->_day;
}
};
int main()
{
Date d1;
d1.Date_Init(30, 10, 2024);
d1.print();
return 0;
}
对于Date类,可以定义上面这种初始化函数。但是如果每次创建对象的时候都像这样去调用该方法设置信息,有些繁琐。那么能否在对象创建时,就将信息设置进去呢?
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个函数的都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
2.2构造函数的特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然叫构造,但是构造函数的主要任务不是开辟空间创建对象,二是初始化对象。其特性为:
- 函数名与类名相同。
- 没有返回值。
- 对象实例化时,由编译器自动调用对应的构造函数。
- 构造函数可以重载。
#include<iostream> using namespace std; class Date { private: int _day; int _month; int _year; public: Date(int day, int month, int year)//带参构造函数 { this->_day = day; this->_month = month; this->_year = year; } Date(){}//无参构造函数 void print() { cout << this->_year << "/" << this->_month << "/" << this->_day; } }; int main() { Date d1(30, 10, 2024); Date d2;//调用无参构造函数时,不要加(),否则变成函数声明了。 d1.print(); return 0; }
- 如果类中没有显式定义构造函数,则C++编译器会自动生成无参的默认构造函数,一旦显示定义任何一个构造函数,编译器就不再生成。
- C++把类型分为内置类型和自定义类型。编译器生成的默认构造函数对内置类型的成员变量不做处理,对自定义类型的成员变量调用自定义类型的默认构造函数。
#include<iostream> using namespace std; class Time { private: int _hour; int _minute; public: Time() { cout << "Time构造函数已调用" << endl; _hour = 1; _minute = 1; } }; class Date { private: int _day; int _month; int _year; Time _t;//自定义类型 public: Date(int day, int month, int year) { this->_day = day; this->_month = month; this->_year = year; } Date(){} void print() { cout << this->_year << "/" << this->_month << "/" << this->_day; } }; int main() { Date d2; return 0; }
- 无参的构造函数和全缺省的参数都成为默认构造函数,并且默认构造函数只能存在一个。
三、析构函数
3.1析构函数的概念
析构函数完成的不是对对象本身的销毁,对象的销毁由编译器完成。析构函数在对象销毁时被自动调用,完成对对象中资源清理。
3.2析构函数的特性
- 析构函数名是在类名前加~。
- 无参数且无返回类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。析构函数不能重载。
- 对象生命周期结束时,C++编译器自动调用析构函数。
- 编译器自动生成的析构函数,对于内置类型的成员,销毁时不需要清理资源,最后系统直接将其内存回收即可,对于自定义类型的成员,会调用这个成员的析构函数。
#include<iostream> using namespace std; class Time { private: int _hour; int _minute; public: ~Time() { cout << "Time析构函数已调用" << endl; _hour = 1; _minute = 1; } }; class Date { private: int _day; int _month; int _year; Time _t; public: Date(int day, int month, int year) { this->_day = day; this->_month = month; this->_year = year; } Date(){} void print() { cout << this->_year << "/" << this->_month << "/" << this->_day; } }; int main() { Date d2; return 0; }
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类,有资源申请时,一定要写,否则会造成资源泄漏。
四、拷贝构造函数
4.1拷贝构造函数的概念
拷贝构造函数是一个特殊的构造函数,它用于创建一个对象作为另一个同类型对象的副本。当你需要通过值传递对象、返回对象、或者以对象为参数调用函数时,编译器可能会自动调用拷贝构造函数。
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
4.2拷贝构造函数的特性
拷贝构造函数也是特殊的成员构造函数,其特性如下:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值的方式会引发无穷递归调用。
#include<iostream) using namespace std; class Date { private: int _day; int _month; int _year; public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date(Date d) { _year = d.year; _month = d.month; _year = d.year; } }; int main() { Date d1; Date d2(d1); return 0; }
这么写是错误的,d2拷贝d1,需要用到拷贝构造函数,在上面的代码中,拷贝构造函数的参数d不是引用类型,而是按值传递。按值传递意味着在调用拷贝构造函数时,会创建d的副本,这又会调用拷贝构造函数,从而导致无限递归,最终导致程序栈溢出并崩溃。
#include<iostream) using namespace std; class Date { private: int _day; int _month; int _year; public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date(const Date& d) { _year = d.year; _month = d.month; _year = d.year; } }; int main() { Date d1; Date d2(d1); return 0; }
拷贝构造函数中,只能使用引用做传递参数。
- 若为显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
#include<iostream> using namespace std; class Time { private: int _hour; int _minute; public: Time() { _hour = 1; _minute = 1; } Time(const Time& t) { _hour = t._hour; _minute = t._minute; cout << "Time::Time(const Time&)" << endl; } }; class Date { private: int _day; int _month; int _year; Time _t; public: Date(int day, int month, int year) { this->_day = day; this->_month = month; this->_year = year; } Date() {} void print() { cout << this->_year << "/" << this->_month << "/" << this->_day; } }; int main() { Date d1; Date d2(d1); return 0; }
上面的代码中,Date类没有显式定义拷贝构造函数,编译器为Date类生成一个默认的拷贝构造函数。在生成的拷贝构造函数中,内置类型是按照字节方式是直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
- 如果类中没有进行资源申请,拷贝构造函数可以不显式定义,但是一旦涉及到资源申请时,则拷贝构造函数是一定要显式定义的,否则就是浅拷贝。下面的代码中,a1对象在构造函数中,默认是申请了10个元素的空间。a2对象使用a1拷贝构造,而Array类没有显示定义拷贝构造函数,而编译器会给Array类自动生成一份默认的拷贝构造函数,默认拷贝构造函数是按值拷贝的,将a1中的内容原封不动的拷贝到a2中,因此a1和a2指向了同一块内存空间。当程序退出时,a2和a1要销毁,等于同一块地址空间被释放了两次。
#include<iostream> using namespace std; class Array { private: int* _array; size_t _size; size_t _capacity; public: Array(size_t capacity = 10) { _array = (int*)malloc(capacity * sizeof(int)); if (_array == nullptr) { perror("malloc申请空间失败"); return; } _size = 0; _capacity = capacity; } void push(const int& data) { _array[_size] = data; _size++; } ~Array() { free(_array); _array = nullptr; _capacity = 0; _size = 0; } }; int main() { Array a1; a1.push(1); a1.push(2); a1.push(3); a1.push(4); a1.push(5); a1.push(6); Array a2(a1);//浅拷贝 return 0; }
- 拷贝构造函数调用的典型场景:使用已存在的对象创建新对象,函数参数类型返回为类类型对象,函数返回值类型为类类型对象。
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year, int minute, int day)
{
cout << "Date(int,int,int):" << this << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d):" << this << endl;
}
~Date()
{
cout << "~Date():" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
Date Test(Date d)
{
Date temp(d);
return temp;
}
int main()
{
Date d1(2022, 1, 13);
Test(d1);
return 0;
}
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
五、赋值运算符重载
5.1运算符重载
C++为了增强函数的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有返回值类型,函数名以及参数列表。
ReturnType operatorOperators(paralist)
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
如果这么写的话,运算符重载成全局函数需要成员变量是公有的,如何保证封装性呢?可以写成友元函数或者重载成成员函数。
#include<iostream>
using namespace std;
class Date
{
public:
bool operator==(const Date& d1)
{
return _year == d1._year && _month == d1._month && _day == d1._day;
}
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
运算符重载需要注意:
- 不能通过来凝结其他符号来创建新的操作符。
- 重载操作符必须有一个类类型参数。
- 用于内置类型的运算符,其含义不能改变。
- 作为类成员函数重载时,其形参看起来比操作数少1,因为成员函数的第一个参数为隐藏的this。
- ".*","::","sizeof","?:","." 这五个运算符不能重载。
5.2赋值运算符重载
- 赋值运算符重载个数:
- 参数类型:const T&,传递引用可以提高传参效率。
- 返回值类型:T&返回引用可以提高返回的效率,有返回值的目的是为了支持连续赋值。
- 检测是否有自己给自己赋值。
- 返回*this:要符合连续赋值的含义。
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date(const Date& d) { _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; } private: int _year; int _month; int _day; };
- 赋值运算符只能重载成类的成员函数不能重载成全局函数。
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } int _year; int _month; int _day; }; // 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数 Date& operator=(Date& left, const Date& right) { if (&left != &right) { left._year = right._year; left._month = right._month; left._day = right._day; } return left; } // 编译失败: // error C2801: “operator =”必须是非静态成员
赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
- 用户没有显示实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
class Time { public: Time() { _hour = 1; _minute = 1; _second = 1; } Time& operator=(const Time& t) { if (this != &t) { _hour = t._hour; _minute = t._minute; _second = t._second; } return *this; } private: int _hour; int _minute; int _second; }; class Date { private: // 基本类型(内置类型) int _year = 1970; int _month = 1; int _day = 1; // 自定义类型 Time _t; }; int main() { Date d1; Date d2; d1 = d2; return 0; }
注意:如果类中未涉及到资源管理,那么赋值运算符是否实现都可以;一旦涉及到资源管理则必须要显式实现。如果没有显示的实现赋值运算符重载,编译器会以浅拷贝的方式实现一份默认的赋值运算符重载。这就会引起一个老生常谈的问题,拷贝过程中会导致两个对象共享同一片地址空间,这会引起两个问题:
- 其中一个对象原来的空间丢失了,导致内存泄漏。
- 共享同一块内存空间,最后销毁时会把同一块内存空间释放两次而引起程序崩溃。
5.3前置++和后置++的重载
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } // 前置++:返回+1之后的结果 // 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率 Date& operator++() { _day += 1; return *this; } // 后置++: // 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载 // C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器会自动传递 // 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存 // 而temp是临时对象,因此只能以值的方式返回,不能返回引用 Date operator++(int) { Date temp(*this); _day += 1; return temp; } private: int _year; int _month; int _day; }; int main() { Date d; Date d1(2022, 1, 13); d = d1++; d = ++d1; return 0; }
- 用户没有显示实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
六、const成员
将const修饰的成员函数称之为const成员函数,cosnt修饰类成员函数实际修饰该成员函数隐含的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 Print() const
{
cout << "Print()const" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
void Test()
{
Date d1(2022, 1, 13);
d1.Print();
const Date d2(2022, 1, 13);
d2.Print();
}
七、取地址及const取地址操作符重载
这两个运算符一般不需要重载,使用编译器生成的默认取地址重载即可,只有特殊情况才需要重载。
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};