- 前言
- 类和对象
- 1、类的6个默认成员函数
- 2、构造函数
- 3、析构函数
- 4、拷贝构造函数
- 5、赋值运算符重载
- 6、日期类的实现
- 7、const成员
- 8、取地址运算符重载
这篇总结是类和对象中最重要的一篇
大家加油噢!!!
类和对象 1、类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?
答案是:不是的,对于任何一个空类,编译器都会自动生成6个默认成员函数
class ClassName
{
//空类
//.........
};
我们应该怎么学习并了解这些默认成员函数呢?
- 学习它的基本语法和特性
- 了解>函数名、参数列表、返回类型的意义
- 我们不显示写时,编译器默认生成的成员函数做了什么操作…
2、构造函数
首先,我们来看以下的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, d2;
d1.Init(2022, 5, 18);
d1.Print();
Date d2;
d2.Init(2022, 5, 20);
d2.Print();
return 0;
}
- 对于Date类,可以通过Init公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,未免有点麻烦,那我们能否在对象创建时,就进行初始化呢?
构造函数的概念
- 构造函数是一个特殊的成员函数
- 名字与类名相同,创建类类型对象时由编译器自动调用
- 保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次
构造函数的特性
构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开辟空间创建对象,而是初始化对象
- 构造函数函数名与类名相同
- 构造函数没有返回值
- 当类对象实例化时,编译器自动调用构造函数进行初始化
- 构造函数可以重载
class Date
{
public:
// 1.无参构造函数
Date() {}
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
//调用无参构造函数
Date d1;
//调用带参的构造函数
Date d2(2015, 1, 1);
//以下代码的函数:声明了d3函数,该函数无参,返回一个Date类型的对象
Date d3();
};
注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
class Date
{
public:
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
// 没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数
Date d;
};
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个
class Date
{
public:
//默认构造函数
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}
//全缺省构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
//以下测试函数能通过编译吗?
void TestDate()
{
Date d1;
}
- 不能通过编译,因为类中有二个默认构造函数,造成二义性
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数
- 在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是初始化后的成员变量却是随机值也就说在这里编译器生成的默认构造函数并没有什么用吗??
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++把类型分成内置类型和自定义类型
- 内置类型就是语法已经定义好的类型:int/char/double/指针…
- 自定义类型就是自己定义的类型:class/struct…
- 再看看上面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数
- 默认生成的构造函数对于内置类型成员变量不做处理,自对于定义类型成员变量才做处理
总结:
- 如果一个类的成员变量全是自定义类型,则我们可以使用默认生成的构造函数
- 如果有内置类型的成员,或者需要显示传参初始化的,则我们必须自己实现构造函数
- 一般情况下,定义一个C++类,都要自己写构造函数。一般只有少数情况可以让编译器默认生成
- 成员变量的命名风格
// 我们看看这个函数,是不是很僵硬?
class Date
{
public:
Date(int year)
{
// 这里的year到底是成员变量,还是函数形参?
year = year;
}
private:
int year;
};
通过上面程序可以看出:
- 编译器会首先选择成员函数进行赋值,而不是函数形参
//所以我们一般都建议这样
class Date
{
public:
Date(int year)
{
_year = year;
}
private:
int _year;
};
//或者这样
class Date
{
public:
Date(int year)
{
m_year = year;
}
private:
int m_year;
};
// 其他方式也可以的,主要看公司要求。一般都是加个前缀或者后缀标识区分就行
3、析构函数
析构函数的概念
- 与构造函数功能相反,析构函数不是完成对象的销毁
- 局部对象销毁工作是由编译器完成的
- 对象在销毁时会自动调用析构函数,完成类的一些资源清理工作
析构函数的特性
析构函数是特殊的成员函数
- 析构函数名是在类名前加上字符 ~
- 无参数无返回值
- 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数
- 对象生命周期结束时,C++编译器自动调用析构函数
注意:析构函数对自定义类型不会做处理,自定义类型会去调用自己的析构函数
class SeqList
{
public:
SeqList(int capacity = 10)
{
//动态开辟空间
_pData = (int*)malloc(capacity * sizeof(int));
_size = 0;
_capacity = capacity;
}
~SeqList()
{
if (_pData)
{
free(_pData); // 释放堆上的空间
_pData = NULL; // 将指针置为空
_capacity = 0;
_size = 0;
}
}
private:
int* _pData;
size_t _size;
size_t _capacity;
};
关于编译器自动生成的析构函数,是否会完成一些事情呢?
class String
{
public:
String(const char* str = "jack")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String()
{
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
class Person
{
private:
String _name;
int _age;
};
int main()
{
Person p;
return 0;
}
通过上面的程序可以看出:
- 默认生成的析构函数,内置类型成员不做处理
- 自定义类型成员会去调用它的析构函数
关于析构函数的调用顺序,跟构造函数一样吗?
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
};
class B
{
public:
~B()
{
cout << "~B()" << endl;
}
};
int main()
{
A a;
B b;
return 0;
}
总结:
- 析构函数的调用顺序与构造函数是相反的
- 因为类对象是在栈中实例化的,栈有"先进后出"的特性
4、拷贝构造函数
拷贝构造函数的概念:
- 参数列表中只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰)
- 用已存在的类类型对象创建新对象时由编译器自动调用
拷贝构造函数的特性:
拷贝构造函数也是一个特殊的成员函数
- 拷贝构造函数是构造函数的一个重载形式
- 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
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;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
注意:参数不加引用时,编译器也会报语法错误
图解无限递归调用过程:
- 若未显示定义,系统生成默认的拷贝构造函数
- 默认的拷贝构造函数对象按内存存储按字节序完成拷贝这种拷贝我们叫做浅拷贝
// 1、内置类型的成员变量会自动完成值拷贝或者叫浅拷贝
class A
{
public:
A(int a = 0)
{
_a = a;
}
private:
int _a;
};
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;
//2、自定义类型的成员,去调用这个成员的拷贝构造
A a;
};
int main()
{
Date d1;
//这里d2调用的默认拷贝构造完成拷贝,d2和d1的值也是一样的
Date d2(d1);
return 0;
}
5、赋值运算符重载
运算符重载的概念:
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似
函数名字为:关键字operator后面接需要重载的运算符符号
函数原型:返回值类型 operator操作符 (参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型或者枚举类型的操作数
- 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的
操作符有一个默认的形参this,限定为第一个形参 - .* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载
运算符重载有三种写法:
- 在全局作用域中定义运算符重载函数
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;
}
void Test()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
//这里等价于operator==(d1, d2)
cout << (d1 == d2) << endl;
}
缺陷:破坏了类的封装,让成员变量可见(从私有变成公有成员)
- 在类中定义运算符重载函数
这是公认标准的写法,没有破坏类的封装
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// bool operator==(Date* const this, const Date& d2)
// 这里需要注意的是,左操作数是this指向的调用函数的对象
bool operator==(const Date& d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
//这里相当于d1.operator==(d2)
cout << (d1 == d2) << endl;
}
- friend修饰类中定义的运算符重载函数
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// bool operator==(Date* const this, const Date& d2)
// 这里需要注意的是,左操作数是this指向的调用函数的对象
friend bool operator==(const Data& d1, const Date& d2);
private:
int _year;
int _month;
int _day;
};
bool operator==(const Data& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
void Test()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
//这里相当于d1.operator==(d2)
cout << (d1 == d2) << endl;
}
缺陷:破坏了类的封装,可以间接的访问类的私有和保护成员
总结:
- 内置类型,可以直接用各种运算符
- 自定义类型,不能直接用各种运算符,为了自定义类型可以使用各种运算符,制定了运算符重载的规则
赋值运算符的重载:
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;
}
注意事项:
- 参数类型
- 返回值
- 检测是否自己给自己赋值
- 返回*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;
}
private:
int _year;
int _month;
int _day;
}
void Test()
{
Data d1(2022, 5, 20);
//这里调用了默认赋值运算符重载函数(按字节序的值进行拷贝),它的值与d1一样
Data d2 = d1;
return 0;
}
那么编译器生成的默认赋值重载函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?
我们会发现下面的程序会崩溃掉!!这里就需要我们以后讲的深拷贝去解决
class String
{
public:
String(const char* str = "jack")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String()
{
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2(s1);
}
为什么会程序会崩溃呢?
- 因为它们只是进行了浅拷贝,二个对象中的指针指向同一块空间
- 当对象销毁时,同一块空间将被析构二次,导致系统崩溃
总结:
- 一般的类,自己生成默认拷贝构造就够用了
- 只有像String这样的类,自己直接管理资源(malloc), 需要自己实现深拷贝
6、日期类的实现
Date.h #include#include using std::cout; using std::cin; using std::endl; class Date { public: bool isLeapYear(int year) { return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); } int GetMonthDay(int year, int month); // 默认生成的析构函数,内置类型成员不做处理,自定义类型成员会去调用它的析构函数 Date(int year = 1, int month = 1, int day = 1); //拷贝构造函数 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; } Date operator+(int day) const; Date& operator+=(int day); Date operator-(int day) const; Date& operator-=(int day); // ++d1 Date& operator++() // 前置 { *this += 1; return *this; } // d1++ Date operator++(int) // 后置 { Date tmp(*this); *this += 1; return tmp; } Date& operator--() // 前置 { *this -= 1; return *this; } Date operator--(int) // 后置 { Date tmp(*this); *this -= 1; return tmp; } // d1 - d2 int operator-(const Date& d) const; bool operator==(const Date& d) const; bool operator<(const Date& d) const; // inline不支持声明和定义分别放到.h 和.cpp // 所以成员函数中要成为inline最好直接在类里面定义 // 类里面定义默认就是inline bool operator>(const Date& d) const { return !(*this <= d); } bool operator>=(const Date& d) const { return !(*this < d); } bool operator!=(const Date& d) const { return !(*this == d); } // d1 <= d2 bool operator<=(const Date& d) const { return *this < d || *this == d; } private: int _year; int _month; int _day; }; Date.cpp #include "Date.h" bool Date::operator<(const Date& d) { if ((_year < d._year) || (_year == d._year && _month < d._month) || (_year == d._year && _month == d._month && d._day < d._day)) { return true; } else { return false; } } // d1 == d2 bool Date::operator==(const Date& d) { return _year == d._year && _month == d._month && _day == d._day; } int Date::GetMonthDay(int year, int month) { assert(year >= 0 && month > 0 && month < 13); const static int monthDayArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; if (month == 2 && isLeapYear(year)) { return 29; } else { return monthDayArray[month]; } } Date::Date(int year, int month, int day) { if (year >= 1 && month <= 12 && month >= 1 && day >= 1 && day <= GetMonthDay(year, month)) { _year = year; _month = month; _day = day; } else { cout << "ڷǷ" << endl; } } Date Date::operator+(int day) { Date ret(*this); ret += day; return ret; } // d1 += day Date& Date::operator+=(int day) { if (day < 0) return *this -= -day; _day += day; while (_day > GetMonthDay(_year, _month)) { _day -= GetMonthDay(_year, _month); _month++; if (_month == 13) { ++_year; _month = 1; } } return *this; } Date Date::operator-(int day) { Date ret = *this; ret -= day; return ret; } // d1 -= day Date& Date::operator-=(int day) { if (day < 0) return *this += -day; _day -= day; while (_day <= 0) { --_month; if (_month == 0) { _month = 12; --_year; } _day += GetMonthDay(_year, _month); } return *this; } // d1 - d2 int Date::operator-(const Date& d) { int flag = 1; Date max = *this; Date min = d; if (*this < d) { min = *this; max = d; flag = -1; } int n = 0; while (min != max) { ++n; ++min; } return n * flag; }
结论:
- 当重载>, >=, <, <=, == , !=运算符时,先重载一对,例如:>和==,然后复用它来进行其他的重载
- 当重载+,+=,-,-=运算符时,先写+=和-=然后复用它重载+,-
如果拷贝构造函数不加const修饰会怎么样呢?
class Date
{
public:
Date operator+(int day)
{
Date ret(*this);
ret += day;
return ret;
}
//未加const修饰
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 5, 19);
Date d2 = d1 + 100;
return 0;
}
不加const修饰会编译不通过
原因:引用权限问题
7、const成员
const修饰类的成员函数
- 将const修饰的类成员函数称之为const成员函数
- const修饰类成员函数,实际修饰该成员函数隐含的this指针
- 表明在该成员函数中不能对类的任何成员进行修改(const 类名* const this)
我们来看看下面的代码:
class Date
{
public:
void Display()
{
cout << "Display ()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
void Display() const
{
cout << "Display () 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;
d1.Display();
const Date d2;
d2.Display();
}
请思考下面的几个问题:
- const对象可以调用非const成员函数吗?
不可以,权限被放大了!!! - 非const对象可以调用const成员函数吗?
可以,权限被缩小了!!! - const成员函数内可以调用其它的非const成员函数吗?
可以,权限被缩小了!!!
5. 非const成员函数内可以调用其它的const成员函数吗?
不可以,权限被放大了!!!
8、取地址运算符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
注意:这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
感谢大家的观看,请多多指教!!!



