目录
一、什么是类?
1.类的概念
● 类可以说是c语言中结构体的扩展,C语言的结构体仅仅是数据成员的集合,而C++的类在此基础上增加了更多强大的功能,例如类中不仅存在数据成员(成员变量),还有作用于这些数据的函数(成员函数)。
● 类为创建对象(类的实例)提供了模板,而且支持继承和多态等面向对象编程的关键特性。
2.类的定义
● class为定义类的关键字,在class后面加所定义类的名字(一般为有意义的名字如:Stack栈,string字符串等等,当然也可以没有什么意义)。后面在{};之中去写入类的成员变量和成员函数等。
● 一般为了区别成员变量和其他变量,会在成员变量加上_或者其他特殊标识,这个没有具体要求。在c++中struct也升级成为了类,不过一般还是推荐使用class去定义类。有很多原因例如c++兼容c语言,但是c中struct之中只能够有成员变量,不能有成员函数,c代码和c++代码混在一起就会出问题和其他方面原因。
class databirthday
{
int datahappy(int year, int month, int day)
{
return year + month + day;
}
int _year;
int _month;
int _day;
};
二、类的基础内容
1.访问限定符
访问限定符是限定类的成员函数和成员变量的访问权限,从而选择性的让成员变量能够直接访问,而不用通过成员函数进行访问。public修饰的成员就可以在类外直接访问,而protected和private修饰的成员在类外不能够被直接访问。例如下面的成员变量_day在主函数的访问方式:
#include<iostream>
using namespace std;
class dataday
{
private:
short _day;
};
int main()
{
dataday a;
a._day = 0;
return 0;
}
这里_day被private修饰就不能够被主函数直接访问,在class类中默认都是被private修饰,struct默认被public修饰这里写出来方便大家理解,但是如果这里的_day被public修饰就可以访问。
注意:访问权限作⽤域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为⽌,如果后⾯没有访问限定符,作⽤域就到}即类结束。
2.类域
● 类定义了一个新的作用域,和之前博文介绍的命名空间域类似,在类外去定义成员的时候,需要使用::作用域操作符去指明成员属于哪个类域。
● 类域影响的是编译的查找规则,如果一个函数是全局函数,但是调用了被private修饰的成员变量就会报错无法访问,而如果该函数在类外定义但是用::作用域操作符声明了类域,那么该函数就不会被认定为全局函数,而是一个类的成员函数,也就可以访问成员变量。例如:
#include<iostream>
using namespace std;
class Stack
{
public:
// 成员函数
void Init(int n = 4);
private:
// 成员变量
int* array;
size_t capacity;
size_t top;
};
// 声明和定义分离,需要指定类域
void Stack::Init(int n)
{
array = (int*)malloc(sizeof(int) * n);
if (nullptr == array)
{
perror("malloc申请空间失败");
return;
}
capacity = n;
top = 0;
}
int main()
{
Stack st;
st.Init();
return 0;
}
上面程序中Init如果不指定类域Stack,那么编译器就把Init当成全局函数,那么编译时,找不到array等成员的声明/定义在哪⾥,就会报错。指定类域Stack,就是知道Init是成员函数,当前域找不到的array等成员,就会到类域中去查找。
3.实例化
所谓实例化就是拿着图纸修房子。图纸只有一种,房子可以有无数个,这里就是拿着类类型去物理内存中创建对象的过程,称为类实例化对象。类是对象进⾏⼀种抽象描述,是⼀个模型⼀样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,⽤类实例化出对象时,才会分配空间。
4.实例化对象大小
由类类型实例化的对象具有物理内存,但是具体的物理内存该包括成员函数和成员变量吗?首先函数被编译之后是一段指令,对象是没有办法去存储的,最多存储的是成员函数的指针,但是如果实例化100个对象甚至更多的对象,那成员函数指针不就被存储成百上千次,太浪费空间了。这里的类对象大小只存储了成员变量。不过也要符合内存对齐规则。
5.内存对齐规则
● 第⼀个成员在与结构体偏移量为0的地址处。
● 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
● 注意:对⻬数=编译器默认的⼀个对⻬数与该成员⼤⼩的较⼩值。VS中默认的对⻬数为8。
● 结构体总⼤⼩为:最⼤对⻬数(所有变量类型最⼤者与默认对⻬参数取最⼩)的整数倍。
● 如果嵌套了结构体的情况,嵌套的结构体对⻬到⾃⼰的最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体的对⻬数)的整数倍。
举个例子:
#include<iostream>
using namespace std;
class A
{
public:
void Print()
{
cout << _ch << endl;
}
private:
char _ch;
int _i;
};
class B
{
public:
void Print()
{
//...
}
};
class C
{};
int main()
{
A a;
B b;
C c;
cout << sizeof(a) << endl;
cout << sizeof(b) << endl;
cout << sizeof(c) << endl;
return 0;
}
计算A类实例化的对象大小的时候,有一个char类型的变量一个int类型的变量,首先第一个成员在与结构体偏移量为0的地址处,也就是_ch在0的地址处,然后_i要对⻬到(对⻬数)的整数倍的地址处。这里int的对齐数是4,vs对齐数是8,小的是4所以对齐数是4,那么_i要在偏移量为4的地址处,再加上int为4字节,结构体总⼤⼩为:最⼤对⻬数(所有变量类型最⼤者与默认对⻬参数取最⼩)的整数倍,也就是char的整数倍8是1的整数倍,所以A类实例化对象大小是8。

计算B类实例化对象大小的时候,没有成员变量只有一个成员函数可以和C类没有成员变量和成员函数去对比验证成员函数是否有内存存在,这里B类和C类对象大小为1,纯粹是为了占位标识对象的存在。
6.this指针
通过同一个类实例化的不同对象去调用相同函数的时候,函数体中没有关于不同对象的区分,那么函数如何知道到底是该访问哪一个对象呢?这里就需要一个隐含的this指针去解决问题。编译器编译后,类的成员函数默认都会在形参第⼀个位置,增加⼀个当前类类型的指针,叫做this指针。类的成员函数中访问成员变量,本质都是通过this指针访问的,C++规定不能在实参和形参的位置显⽰的写this指针(编译时编译器会处理),但是可以在函数体内显⽰使⽤this指针。
如下:
#include<iostream>
using namespace std;
class Date
{
public:
// void Init(Date* const this, int year, int month, int day)
void Init(int year, int month, int day)
{
_year = year;
this->_month = month;
this->_day = day;
}
void Print()
{
cout << this->_year << "/" << this->_month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
d1.Init(2025, 7, 7);
d1.Print();
d2.Init(2025, 7, 8);
d2.Print();
return 0;
}
这里在成员函数中,可以将this指针写出来,也可以不用写默认也是通过this指针去访问成员变量的,this指针就分别指向的d1和d2的地址。
三、类的重要内容(默认成员函数)
默认成员函数是 C++ 类中一类特殊的成员函数。当用户未显式定义这些函数时,编译器会自动生成它们。一个类总共有 6 个默认成员函数,不过其中前 4 个最为重要,后两个取地址操作符重载的默认成员函数重要性较低,简单了解就行。

1.构造函数
构造函数虽然叫构造函数,但是它并不是开空间创建对象,而是对象实例化的时候初始对象。在c语言中我们常常自己写Init函数取初始化栈或者队列等等,构造函数的作用也就类似Init。主要可以分为默认构造和普通构造,默认构造有几种,总结出来就是不需要传实参的为默认构造,需要传实参的为普通构造。先简述一下特点再仔细阐明两种构造。
特点:1. 函数名与类名相同,⽆返回值。(返回值啥都不需要给,也不需要写void)
2. 对象实例化时系统会⾃动调⽤对应的构造函数。
3. 构造函数可以重载。
4. 如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦⽤⼾显式定义编译器将不再⽣成。
● 默认构造函数:无参构造函数,全缺省构造函数和编译器默认生成的构造函数。默认构造函数只能存在一个,因为如果都存在,不传实参调用的时候就会出现问题。
● 普通构造函数:有参构造函数,需要传实参才能够调用,可以和默认构造函数同时存在构成函数重载,不过都需要满足函数重载的条件。
● 当我们不写构造函数的时候,编译器生成的默认构造函数对内置类型的变量的初始化是没有要求的,也就是编译器可能初始化也可能不管。但是对于自定义类型的成员变量,要求调用成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤初始化列表才能解决,在下面会介绍初始化列表。
class MyClass {
public:
// 默认构造函数(无参)只能存在一个
MyClass() { /* ... */ }
MyClass(int x=10) { /* ... */ }
// 普通构造函数(带参数)
MyClass(int x) { /* ... */ }
MyClass(int x, double y) { /* ... */ }
};
2.初始化列表
我们实现构造函数时,初始化成员变量主要使⽤函数体内赋值,构造函数初始化还有⼀种⽅式,就是初始化列表。初始化列表可以说是构造函数的一部分,是最开始初始化的地方,而函数体内的赋值是成员变量的再赋值的地方。初始化列表的使⽤⽅式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式。
特点:1.⽆论是否显⽰写初始化列表,每个构造函数都有初始化列表; ⽆论是否在初始化列表显⽰初始化成员变量,每个成员变量都要⾛初始化列表初始化。
2.每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地⽅。
3.引⽤成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进⾏初始化,否则会编译报错。
4.C++11⽀持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显⽰在初始化列表初始化的成员使⽤的。
5.尽量使⽤初始化列表初始化,因为那些你不在初始化列表初始化的成员也会⾛初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会⽤这个缺省值初始化。如果你没有给缺省值,对于没有显⽰在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有显⽰在初始化列表初始化的⾃定义类型成员会调⽤这个成员类型的默认构造函数,如果没有默认构造会编译错误。
6.初始化列表中按照成员变量在类中声明顺序进⾏初始化,跟成员在初始化列表出现的的先后顺序⽆关。建议声明顺序和初始化列表顺序保持⼀致。
#include<iostream>
using namespace std;
class Date
{
public:
Date(int& x, int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
, _ref(x)
, _n(1)
{
}
private:
int _year;
int _month;
int _day;
int& _ref; // 引⽤
const int _n; // const
};
int main()
{
int i = 0;
Date d1(i);
return 0;
}
3.析构函数
析构函数与构造函数功能相反,析构函数并不是完成对象本身的销毁,就像构造函数并不是完成对象内存的创建一样,局部对象都是存在栈帧里面的,我们不需要管,函数结束栈帧就销毁了。C++规定对象在销毁时会⾃动调⽤析构函数,完成对象中资源的清理释放⼯作。严格说如果对象中没有申请资源也就不需要自定义析构函数。
特点:1. 析构函数名是在类名前加上字符~,⽆参数⽆返回值。(这⾥跟构造类似)
2. ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数。
3. 对象⽣命周期结束时,系统会⾃动调⽤析构函数。
4. 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数。
5. 还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类型成员⽆论什么情况都会⾃动调⽤析构函数。
6.⼀个局部域的多个对象,C++规定后定义的先析构。
class Point {
private:
int x, y; // 基本类型,无需手动释放
public:
// 默认析构函数足够,无需自定义
};
class Stack
{
public:
Stack(int n = 4)
{
_a = new int[n] {1,2,3,4};//类对象中申请了空间资源
_capacity = n;
_top = 0;
}
~Stack()
{
delete[]_a;//释放类对象中申请的空间资源
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
size_t _capacity;
size_t _top;
};
int main()
{
Stack c;
return 0;
}
这两个例子就说明如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数。如果默认⽣成的析构就可以⽤,也就不需要显⽰写析构,但是如果有资源申请就需要写析构函数防止内存泄漏。
4.拷贝构造函数
拷贝构造函数是构造函数的一个重载,拷贝构造函数可以通过一个类类型的实例化对象来初始化一个类类型的另一个实例化对象。它的第⼀个参数是⾃⾝类类型的引⽤,且任何额外的参数都有默认值,则此构造函数也叫做拷⻉构造函数,也就是说拷⻉构造是⼀个特殊的构造函数。
特点:1. 拷⻉构造函数是构造函数的⼀个重载。
2. 拷⻉构造函数的第⼀个参数必须是类类型对象的引⽤,使⽤传值⽅式编译器直接报错,因为语法逻辑上会引发⽆穷递归调⽤。拷⻉构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引⽤,后⾯的参数必须有缺省值。
3. C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥⾃定义类型传值传参和传值返回都会调⽤拷⻉构造完成。
4. 若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构造。
● 浅拷贝:对于浅拷贝的理解可以说是再创建了一个变量去指向的同一块空间,指向的地址和内容都是一模一样的,在很多情况浅拷贝就会因为这些特性出现问题。
● 深拷贝:对于深拷贝的理解可以说是再创建了一个变量指向的另一块空间,指向的地址不同,但是内容是相同的。
5.类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完成需要的拷⻉,所以不需要我们显⽰实现拷⻉构造。但是成员变量指向了资源,编译器⾃动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就需要显⽰写拷⻉构造,否则就不需要。
6.传值返回会产⽣⼀个临时对象调⽤拷⻉构造,传值引⽤返回,返回的是返回对象的别名(引⽤),没有产⽣拷⻉。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤ 引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以减少拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回。
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, 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(Date* d)
{
_year = d->_year;
_month = d->_month;
_day = d->_day;
}
private:
int _year;
int _month;
int _day;
};
Date& Func2()
{
Date tmp(2024, 7, 5);
return tmp;
}
int main()
{
Date d1(2024, 7, 5);
//这⾥可以完成拷⻉,但是不是拷⻉构造,只是⼀个普通的构造
Date d2(&d1);
//这样写才是拷⻉构造,通过同类型的对象初始化构造,⽽不是指针
Date d3(d1);
// 也可以这样写,这⾥也是拷⻉构造
Date d4 = d1;
// Func2返回了⼀个局部对象tmp的引⽤作为返回值
// Func2函数结束,tmp对象就销毁了,相当于了⼀个野引⽤
Date ret = Func2();
return 0;
}
5.运算符重载
当运算符被⽤于类类型的对象时,C++语⾔允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载,若没有对应的运算符重载,则会编译报错。
注意:1.运算符重载是具有特殊名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成。和其 他函数⼀样,它也具有其返回类型和参数列表以及函数体。
2.重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数。
3.如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算 符重载作为成员函数时,参数⽐运算对象少⼀个。
4. .* :: sizeof ?: . 注意以上5个运算符不能重载。
5.重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,⽆法很好的区分。 C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,⽅便区分。
6.重载>>和<<时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调⽤时就变成了对象<<cout,不符合习惯和可读性。重载为全局函数把ostream/istream放到第一个形参位置就可以了,第二个形参位置当类类型对象,不过要让其成为类的友元函数,后面会介绍。
下面是关于日期计算类的运算符重载:
#include<iostream>
using namespace std;
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
bool CheckDate()//检查日期是否合法
{
if (_month < 1 || _month>12 || _day<1 || _day>GetMonthDay(_year, _month))
{
return false;
}
else
{
return true;
}
}
// 获取某年某月的天数
int GetMonthDay(int year, int month)
{
static int a[13] = { -1, 31, 28, 31, 30, 31, 30,31, 31, 30, 31, 30, 31 };
if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year
% 400 == 0))
{
return 29;
}
else if(month==2)
{
return 28;
}
return a[month];
}
void Getyear()//判断某年是闰年还是平年
{
if ((_year % 4 == 0 && _year % 100 != 0) || (_year % 400 == 0))
{
cout << _year << "是闰年" << endl;
}
else
{
cout << _year << "是平年" << endl;
}
}
void dateflash(int year,int month,int day)//日期更新
{
_year = year;
_month = month;
_day = day;
if (!CheckDate())
{
cout << "非法日期:";
ppfrint();
}
}
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
if (!CheckDate())
{
cout << "非法日期:";
ppfrint();
}
}
// 拷贝构造函数
// d2(d1)
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
// 析构函数
~Date()
{
_year = _month = _day = 0;
}
// 日期+=天数
Date& operator+=(int day)
{
if (day > 0)
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month > 12)
{
_year++;
_month = 1;
}
}
}
else
{
*this -= (-day);
}
return *this;
}
// 日期+天数
Date operator+(int day)
{
Date tmp = *this;
tmp += day;
return tmp;
}
// 日期-天数
Date operator-(int day)
{
Date tmp = *this;
tmp -= day;
return tmp;
}
// 日期-=天数
Date& operator-=(int day)
{
if (day > 0)
{
_day -= day;
while (_day <= 0)
{
if (_month == 1)
{
_day += GetMonthDay(_year, 12);
_month--;
}
else
{
_day += GetMonthDay(_year, _month - 1);
_month--;
}
if (_month < 1)
{
_year--;
_month = 12;
}
}
}
else
{
*this += (-day);
}
return *this;
}
// 前置++
Date& operator++()
{
*this += 1;
return *this;
}
// 后置++
Date operator++(int)
{
Date tmp = *this;
*this += 1;
return tmp;
}
// 后置--
Date operator--(int)
{
Date tmp = *this;
*this -= 1;
return tmp;
}
// 前置--
Date& operator--()
{
*this -= 1;
return *this;
}
// >运算符重载
bool operator>(const Date& d)
{
if (_year > d._year)
{
return true;
}
else if (_year == d._year)
{
if (_month > d._month)
{
return true;
}
else if (_month == d._month)
{
if (_day > d._day)
{
return true;
}
else
{
return false;
}
}
else
{
return false;
}
}
else
{
return false;
}
}
// ==运算符重载
bool operator==(const Date& d)
{
if (_year == d._year && _month == d._month && _day == d._day)
{
return true;
}
else
{
return false;
}
}
// >=运算符重载
bool operator >= (const Date& d)
{
return *this > d || *this == d;
}
// <运算符重载
bool operator < (const Date& d)
{
if ((*this) == d)
{
return false;
}
else
{
return !((*this) > d);
}
}
// <=运算符重载
bool operator <= (const Date& d)
{
return *this < d || *this == d;
}
// !=运算符重载
bool operator != (const Date& d)
{
return !((*this) == d);
}
// 日期-日期 返回天数2025.3.1 2024.3.1
int operator-(const Date& d)
{
int tmp = 0;
Date hmp = *this;
while (hmp._year != d._year)
{
if (hmp._year > d._year)
{
if ((hmp._year % 4 == 0 && hmp._year % 100 != 0) ||
(hmp._year % 400 == 0))
{
tmp += 366;
}
else
{
tmp += 365;
}
hmp._year--;
}
else
{
if (((hmp._year + 1) % 4 == 0 &&
(hmp._year + 1) % 100 != 0) || ((hmp._year + 1) % 400 == 0))
{
tmp += (-366);
}
else
{
tmp += (-365);
}
hmp._year++;
}
}
while (hmp._month != d._month)
{
if (hmp._month > d._month)
{
tmp += GetMonthDay(hmp._year,hmp._month-1);
hmp._month--;
}
else
{
tmp-= GetMonthDay(hmp._year,hmp._month);
hmp._month++;
}
}
while (hmp._day != d._day)
{
if (hmp._day > d._day)
{
tmp += (hmp._day - d._day);
hmp._day = d._day;
}
else
{
tmp -= (hmp._day - d._day);
hmp._day = d._day;
}
}
return tmp;
}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
while (1)
{
cout << "请输入年月日:";
in >> d._year >> d._month >> d._day;
if (!d.CheckDate())
{
cout << "输入为非法日期" << endl;
cout << "请重新输入" << endl;
}
else
{
break;
}
}
return in;
}
6.赋值运算符重载
赋值运算符重载是⼀个默认成员函数,它也是一种特殊的运算符重载。⽤于完成两个已经存在的对象直接的拷⻉赋值,这⾥要注意跟拷⻉构造区分,拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象。而且赋值运算符重载必须成成员函数。
特点:1. 有返回值,且建议写成当前类类型引⽤,引⽤返回可以提⾼效率,有返回值⽬的是为了⽀持连续赋值场景。
2. 没有显式实现时,编译器会⾃动⽣成⼀个默认赋值运算符重载,默认赋值运算符重载⾏为跟默认拷⻉构造函数类似,对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤它的赋值重载函数。
3.这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就需要显⽰写赋值运算符重载,否则就不需要。
如上面的日期类中的赋值运算符重载:
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
7.const成员函数
将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后 ⾯。const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进⾏修改。 如下面对日期类进行打印的时候不会对类的任何成员进行修改,所以最好用const修饰防止被成员被修改。
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
8.取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器⾃动⽣成的就可以够我们⽤了,不需要去显⽰实现。除⾮⼀些很特殊的场景,⽐如我们不想让别⼈取到当前类对象的地址,就可以⾃⼰实现⼀份,胡乱返回⼀个地址。
class Date
{
public:
Date* operator&()
{
return this;
// return nullptr;
}
const Date* operator&()const
{
return this;
// return nullptr;
}
Date* operator&()//返回一个错误的地址误导
{
return 0x00430A11;
// return nullptr;
}
private:
int _year; // 年
int _month; // ⽉
int _day; // ⽇
};
四、类的相关内容
1.类型转换
C++⽀持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。因为这里的转换其实就是由内置类型的值去调用构造函数构造一个这个值的类类型。构造函数前⾯加explicit就不再⽀持隐式类型转换。类类型的对象之间也可以隐式转换,需要相应的构造函数⽀持,原来也是一样的。
class xiaoye
{
public:
int value;
// 允许从 int 到 xiaoye 的隐式转换
xiaoye(int val)
: value(val)
{}
};
void func(xiaoye obj)
{
// 处理 xiaoye 对象
}
int main() {
func(42); // 隐式转换:int(42) -> xiaoye(42)
xiaoye obj = 100; // 隐式转换:int(100) -> xiaoye(100)
}
2.static成员(静态成员)
⽤static修饰的成员变量,称之为静态成员变量。静态成员变量⼀定要在类外进⾏初始化,是因为静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区,这里静态成员变量就不会走初始化列表去初始化所以要在外面初始化。
⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。静态成员函数中可以访问其他的静态成员,但是不能访问⾮静态的,因为没有this指针。⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数,这里静态成员变量就类似于全局变量,不过受到了类域的限制。
注意:1.突破类域就可以访问静态成员,可以通过类名::静态成员或者对象.静态成员来访问静态成员变量和静态成员函数。
2.静态成员也是类的成员,受public、protected、private访问限定符的限制。
3.静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不⾛构造函数初始化列表。
#include<iostream>
using namespace std;
class A
{
public:
A()
{
++_scount;//随意访问
}
A(const A& t)
{
++_scount;
}
~A()
{
--_scount;
}
static int GetACount()
{
return _scount;
}
private:
// 类⾥⾯声明
static int _scount;
};
// 类外⾯初始化
int A::_scount = 0;
int main()
{
//cout << A::_scount << endl;编译报错:error C2248: “A::_scount”: ⽆法访问 private 成员(在“A”类中声明)
return 0;
}
3.友元
当类的成员变量被限定符限定为私有或者保护成员的时候,在类外的其他函数中或者其他类中想直接访问该类的成员变量是会编译报错的,没办法访问。这个时候就出现了友元函数和友元类,在函数声明或者类声明的前⾯加friend,并且把友元声明放到⼀个类的⾥⾯。
注意:1.外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数。
2.友元函数可以在类定义的任何地⽅声明,不受类访问限定符限制。
3.⼀个函数可以是多个类的友元函数。
4.友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。
5.友元类的关系是单向的,不具有交换性,⽐如A类是B类的友元,但是B类不是A类的友元。
6.友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元。
7.有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多⽤。
友元函数例子:
#include<iostream>
using namespace std;
class Rectangle
{
public:
Rectangle(int w, int h)
: _width(w)
, _height(h)
{}
// 声明友元函数:可以访问私有成员
friend int calculateArea(const Rectangle& rect);
private:
int _width;
int _height;
};
// 友元函数实现:直接访问Rectangle的私有成员
int calculateArea(const Rectangle& rect)
{
return rect._width * rect._height; // 无需getter方法
}
int main() {
Rectangle rect(5, 10);
cout<< calculateArea(rect); // 输出: 50
}
友元类例子:
#include<iostream>
using namespace std;
class Time
{
public:
Time(int h, int m)
: hours(h)
, minutes(m)
{}
// 声明友元类:Clock可以访问Time的私有成员
friend class Clock;
private:
int hours;
int minutes;
};
class Clock {
public:
static void displayTime(const Time& t)
{
cout << t.hours << " hours, " << t.minutes << " minutes";
}
static void addOneHour(Time& t)
{
t.hours++; // 直接修改私有成员
}
};
int main() {
Time t(3, 45);
Clock::displayTime(t); // 输出: 3 hours, 45 minutes
Clock::addOneHour(t);
Clock::displayTime(t); // 输出: 4 hours, 45 minutes
}
4.内部类
如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独⽴的类,跟定义在全局相⽐,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。内部类默认是外部类的友元类。
注意:内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使⽤,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地⽅都⽤不了。
#include<iostream>
using namespace std;
class A
{
private:
static int _k;
int _h = 1;
public:
class B // B默认就是A的友元
{
public:
void foo(const A& a)
{
cout << _k << endl; //OK
cout << a._h << endl; //OK
}
int _b1;
};
};
int A::_k = 1;
int main()
{
cout << sizeof(A) << endl;
A::B b;
A aa;
b.foo(aa);
return 0;
}
5.匿名对象
匿名对象顾名思义是没有名字的对象,⽤类型(实参)定义出来的对象叫做匿名对象,相⽐之前我们定义的类型对象名(实参)定义出来的叫有名对象。匿名对象⽣命周期只在当前⼀⾏,⼀般临时定义⼀个对象当前⽤⼀下即可,就可以定义匿名对象。
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
class Solution
{
public:
int Sum_Solution(int n)
{
//...
return n;
}
};
int main()
{
A aa1;
//A aa1();不能这么定义对象,因为编译器⽆法识别是⼀个函数声明,还是对象定义
// 但是我们可以这么定义匿名对象,匿名对象的特点不⽤取名字,
// 但是他的⽣命周期只有这⼀⾏,我们可以看到下⼀⾏他就会⾃动调⽤析构函数
A();
A(1);
A aa2(2);
// 匿名对象在这样场景下就很好⽤,当然还有⼀些其他使⽤场景
Solution().Sum_Solution(10);
return 0;
}
五、总结
类对于c++的学习至关重要,是后面的一些标准库的学习的基础中基础,不然根本看不懂标准库的内容。可能我的解释并不是很繁琐不是清楚请见谅,我是希望尽可能的表达完整,将每个知识点的相关知识也说清楚。希望这篇博文可以帮你更好的理解和学习类,如果我有错误的地方也希望能够温柔的指出来,我们共同进步!

2677

被折叠的 条评论
为什么被折叠?



