1、类和对象
C++中通过class定义类
class A
{
int a;
}; // 定义一个A类型的类
通过类来定义对象
A a; // 定义一个A类型的对象
类是一张蓝图,是抽象的。而对象是根据蓝图真正建造出来的建筑,是具象的。
对象是类的实体化
2、类的限制修饰符
类有三种修饰符:public、private和protected
public表示公有,即类内外都可以访问
private表示私有,即只能类内成员访问
protected表示受保护的,也是只能类内成员访问
从简单理解类的角度来看,可以认为后两者是一样的,他们的区别主要在于继承
class Person
{
public:
void PrintPersonInfo()
{
for (int i = 0; i < 20; ++i)
printf("%c ", _name[i]);
printf("\nage: %d", _age);
}
private:
char _name[20];
char _gender[3];
int _age;
};
在类中,类内成员函数是可以访问类内成员变量的,访问限定符是限制类外对类内的访问
访问限定符只在编译期有效,真正代码映射到内存后,是没有用的
3、计算类的大小
计算类的大小就需要考虑两个因素:成员函数和成员变量,因为类中就只有这俩哥们
但是成员函数是不占用空间的,这是因为C++中类成员函数的存储方式是:所有成员函数均存放在公共代码区,在编译期间就确定了这些成员函数的地址
对于成员函数的存储方式,要注意的是:
1. 成员函数是属于整个类的,和一个具体的对象无关
2. 计算类的大小不包含成员函数,只需要考虑成员变量即可
因此计算类的大小就和C语言中计算结构体大小类似
比如说这个类
class A
{
public:
void print();
int _a;
char _c;
}
根据结构体对齐规则,就可以知道大小应该是12字节
结构体对齐规则:
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的对齐数为8- 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是
所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
理解成员函数的存储方式
class A
{
public:
void print()
{
return;
}
};
int main() {
A* a = nullptr;
a->print();
cout << "a->print() execute" << endl;
return 0;
}
代码运行结果
a->print() execute
这段代码之所以能够正常运行,是因为函数的地址和对象是无关的,函数的地址在编译期间便已经确定,因此当执行a->print()
的时候,并不需要对nullptr解引用,而是直接call 对应的成员函数地址
空类和空类所定义的对象占用1字节空间,用来标识对象以及类是存在的(空类指没有成员变量的类)
4、this指针
this指针是对象的地址,用来标识一个对象
由于成员函数是所有对象共享的,而成员函数很多时候需要去访问成员变量(不访问成员变量放类里面干啥)
class A
{
public:
void print()
{
cout << _a << endl;
}
int _a;
};
int main() {
A a1;
cout << a1._a << endl;
return 0;
}
比如这里a1去访问print(),而print是需要查看a1的_a的,这个_a是存在于a1对象中的,因此成员函数必须要有a1对象的地址才能够去访问到_a变量。
由此可以看出,成员函数,要具有访问任意一个对象的能力,因此就需要this指针
成员函数其实有一个隐藏的参数,就是this指针,以上面的class A中的print为例,其实它的形式是:
void print(const A* this)
{
cout << this._a << endl;
}
每次a1调用成员函数,都会将自己的地址传递给this,因此成员函数就具有了访问对象的能力
5、几种特殊的成员函数
5.1、构造函数
构造函数只负责对象的初始化,并不负责对象空间的开辟
构造函数的名字和类名相同,无返回值,参数自定义
如果我们不自己写构造函数,编译器才会默认生成一个构造函数,这个构造函数对内置类型(int/double/char等)不做处理,对自定义类型去调用它的构造函数
构造函数支持重载和缺省参数
默认构造函数指不需要传参数就能够调用的构造函数
默认构造函数包括编译器生成的、无参构造和全缺省构造,至少要包含一个默认构造函数,当然,第一个默认构造不可能与后两者共存,后两者也不建议同时写,因为调用会出歧义。
class A
{
public:
int _a;
int _b;
int _c;
};
对于这个类来说,下面的都是默认构造函数
A() {} // 无参
A(int a = 1) {} // 全缺省
A(int b = 1) {} // 全缺省
A(int c = 1) {} // 全缺省
A(int a = 1, int b = 2) {} // 全缺省
A(int a = 2, int c = 1) {} // 全缺省
A(int b = 2, int c = 1) {} // 全缺省
C++11补丁
构造函数按理来说不应该不处理内置类型的参数,所以c++11打了一个补丁,支持在成员变量声明的时候给予缺省值(不是赋值)
class A
{
public:
int _a = 1;
int _b = 2;
int _c = 3;
};
5.2、析构函数
析构函数并不是完成对象本身的清理工作,而是完成对象中资源的清理
析构函数名字为 ~类名
,无参数,无返回值,析构函数不支持重载
我们不写析构函数,编译器会给我们默认生成一个析构函数,这个析构函数对于自定义类型调用其析构函数,内置类型不做处理(因为内置类型自己会销毁)
这里的建议是,如果类中申请过资源,对象销毁后,资源仍还在(堆上),那么就需要自己写析构函数对齐进行清理
5.3、拷贝构造函数
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
- 拷贝构造函数是构造函数的一个重载形式,所以,拷贝构造也是构造函数
- 拷贝构造的形参只有一个,且必须是引用,否则会引发无穷递归
以下面的代码来解释一下为什么会引发无穷递归
class Date
{
public:
Date(const Date date)
{
...
}
};
int main()
{
Date date;
Date b(date);
return 0;
这里的A b(a)
语句将a传给一个Date类型的形参,需要进行拷贝构造来构造出a,拷贝构造需要传参,传参又是拷贝构造,就成了死结
我们不写,编译器会默认生成一个拷贝构造函数,这个拷贝构造函数执行的是浅拷贝
深浅拷贝的问题
深拷贝:对于指针等资源,不是单纯的拷贝值,需要为拷贝重新申请一份资源
浅拷贝:仅拷贝值,又称值拷贝
查看下面这段代码
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
这个类中,在堆上申请了一个数组,由于执行的是默认拷贝构造,会把s1中的 _array 的值拷贝给s2,由于析构函数会free(_array), 所以_array就会被free两次,程序会崩溃
所以对于这种,还是得用深拷贝,为s2单独开辟资源
因此,如果需要深拷贝,那么就需要我们自定义拷贝构造,否则不需要
5.4、运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
注意:运算符重载不一定是类的成员函数,它可以声明定义在类外,和类是两个独立个体
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
.
*
::
sizeof
?:
.
注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
这里通过一个日期类来阐述运算符重载的使用
class Date
{
public:
// 全缺省默认构造
Date(int year = 2023, int month = 7, int day = 4)
{
_year = year;
_month = month;
_day = day;
}
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);
}
int main()
{
Date d1;
Date d2;
cout << (d1 == d2) << endl; // d1传递给第1个形参,d2传递给第2个形参
return 0;
}
这里把运算符重载写到了类外面,同样也可以写在类内,写在类内,就会多一个隐藏的this指针的形参,所以就只需要额外写一个形参即可
写了一段完整的日期类代码,如下示
#include <iostream>
#include <cstdio>
using namespace std;
int monthToDay[] = { -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
class Date
{
public:
// 全缺省默认构造
Date(int year = 2023, int month = 7, int day = 4)
{
// 检验日期是否合法
if ((IsLeapYear(year) && month == 2 && day > 29)
|| (!IsLeapYear(year) && month == 2 && day > 28)
|| ((month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12) && day > 31)
|| ((month == 2 || month == 4 || month == 6 || month == 9 || month == 11) && day > 30)
|| year < 1 || month < 0 || day < 0 || month > 12)
{
cout << "非法日期!!!" << endl;
exit(-1);
}
_year = year;
_month = month;
_day = day;
}
int GetYear() const
{
return _year;
}
int GetMonth() const
{
return _month;
}
int GetDay() const
{
return _day;
}
// 判断是否是闰年
bool IsLeapYear(int year) const
{
return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
}
// 支持运算符重载
bool operator==(const Date& d2) const
{
return ((_year == d2.GetYear()) &&
(_month == d2.GetMonth()) &&
(_day == d2.GetDay()));
}
bool operator!=(const Date& d2) const
{
return !((*this) == d2);
}
void operator=(const Date& d)
{
_year = d.GetYear();
_month = d.GetMonth();
_day = d.GetDay();
}
// 加上一个天数
Date operator+(int newDay) const
{
// 计算年月日
int day = _day + newDay;
int month = _month;
int year = _year;
// 先判断年,再判断月
if (IsLeapYear(year)) monthToDay[2] = 29;
else monthToDay[2] = 28;
while (day > monthToDay[month])
{
day -= monthToDay[month];
++month;
if (month > 12)
{
month -= 12;
++year;
if (IsLeapYear(year)) monthToDay[2] = 29;
else monthToDay[2] = 28;
}
}
Date d(year, month, day);
return d;
}
// -
Date operator-(int newDay) const
{
int day = _day;
int month = _month;
int year = _year;
if (IsLeapYear(year)) monthToDay[2] = 29;
else monthToDay[2] = 28;
while (day < newDay)
{
newDay -= day;
--month;
if (month < 1)
{
--year;
if (year < 0)
{
cout << "运算非法!!" << endl;
exit(1);
}
month = 12;
if (IsLeapYear(year)) monthToDay[2] = 29;
else monthToDay[2] = 28;
}
day = monthToDay[month];
}
day -= newDay;
Date d(year, month, day);
return d;
}
// +=
Date& operator+=(int newDay)
{
(*this) = (*this) + newDay;
return (*this);
}
//
// -=
Date& operator-=(int newDay)
{
(*this) = (*this) - newDay;
return (*this);
}
// >
bool operator>(const Date& d1) const
{
if (_year > d1.GetYear()) return true;
else if (_year < d1.GetYear()) return false;
else
{
if (_month > d1.GetMonth()) return true;
else if (_month < d1.GetMonth()) return false;
else
{
if (_day > d1.GetDay()) return true;
else return false;
}
}
}
// <
bool operator<(const Date& d1)
{
return (d1 > (*this)) && (d1 != (*this));
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
d1 += 1000;
printf("%d : %d : %d\n", d1.GetYear(), d1.GetMonth(), d1.GetDay());
return 0;
}
测试日期类是否正确: https://time.org.cn/riqi/