目录
一、什么是类和对象
1.1 class和struct定义类
1.1.1 定义类
class为定义类的关键字,格式为class 名字 {里面是类的主体};(这个分号不能省略)类的主体可以写函数,而这里面函数会默认为内联函数,如果不想用内联函数,就放入.h文件中声明然后.cpp里面定义即可。在C++中struct也可以是类,也就是说struct也可以定义函数了。类里面一般有函数和变量,函数称为类的方法或成员函数,而变量就称为类的属性或成员变量。
1.1.2 限制符封装
而为了限制成员变量和成员方法,这里用到访问限定符public表示公开,类的外面也可以访问,而protect和private修饰的成员在类外不能直接访问,只能说类内使用(后续会将区别)。权限访问作用域是遇到下一个访问限定符结束,如果没有遇到,则到执行完整个类。
在class定义类和struct定义类中,class没有访问限定符时默认是private,而struct没有访问限定符默认是public公开类。
class A
{
//默认private
};
struct B
{
//默认public
};
1.2 类和对象的理解
类域和命名空间域本质都是使用一个域来避免其他域的影响。而类域是解决的是类与类之间的命名冲突。命名空间域解决的是全局函数/变量/类型的命名冲突。类域的出现是因为一个类中出现名字冲突,可以设置另一个类域来避免冲突。命名空间域是因为一个全局域中出现命名冲突,可以设置一个命名空间域来避免和全局域之间冲突。
看一个变量是声明还是定义就看它是否为分配空间。在类中包含的成员变量是声明而不是定义,所以在跳出这个类时,不能直接使用访问。在类中整体定义,此时称谓类的实例化对象。类概念很抽象,具体话来说类相当于一个图纸,而实例化对象就是房子,通过类来构造出的实体房子(实例化对象),图纸不能住人,而实体房子能住人(实例化对象才是给成员变量分配空间)。
#include<iostream>
using namespace std;
class A//图纸
{
public:
void Init(int a1 = 10, int a2 = 20)
{
_a1 = a1;
_a2 = a2;
}
void Print()
{
cout << _a1 << _a2 << endl;
}
private:
//声明(未分配空间)
int _a1;
int _a2;
};
int main()
{
A aa1;//实例化对象(房子)已分配空间
return 0;
}
1.3 类和对象的大小计算
计算一个类的内存时,和struct一样都要符合内存对齐(4条规则,具体请看struct结构体、union联合体和枚举_struct union-CSDN博客),只不过类中存在成员函数,计算过程中不算成员函数,也就是说类的空间大小就是由成员变量(只存成员变量,不存成员函数)。
解释:第一个实例化对象正常使用该成员函数和成语变量,第二个实例化对象在使用时成员函数还是之前那个函数地址,但是成员变量就变了,因为成员需要不同空间存储独立的值,而函数指针/函数是一样的,不需要独立空间,是一个全局唯一一个的地址,只需要一个链接即可。
#include<iostream>
using namespace std;
class A
{
public:
void Init(int a1 = 10, int a2 = 20)
{
_a1 = a1;
_a2 = a2;
}
void Print()
{
cout << _a1 << _a2 << endl;
}
private:
//声明
int _a1;
int _a2;
};
int main()
{
A aa1;//实例化对象
//两种计算方式
cout<< sizeof(aa1) << endl;//打印8
cout<< sizeof(A) << endl;//打印8
aa1.Init(1, 2);
aa1.Print();
A aa2;//实例化对象
cout<< sizeof(aa2) << endl;//打印8
aa2.Init(1, 2);
aa2.Print();
return 0;
}


对于没有成员变量的类对象,空间大小为1,是为了占位进行区分实例化对象地址(一个类名为B里面只有一个成员函数或者没有成员函数和成员变量,进行实例化对象b1和b2,然后这两个进行区分b1和b2地址会不同),用于表示对象存在。
#include<iostream>
using namespace std;
class B
{
public:
void Init(int b1 = 10, int b2 = 20)
{
//....
}
void Print()
{
//....
}
private:
};
int main()
{
cout << sizeof(B) << endl;
B b1;
cout << &b1 << endl;
A b2;
cout << &b2 << endl;
return 0;
}

1.4 this关键字
this关键字,是编译器处理,调用类中成员函数时编译器自动处理将成员函数的形参和调用的实参补充(实参指的是实例化对象的地址,形参则为 类名* const this),并且在成员函数内调用过程编译器也会自动处理为this->成员变量(除了在成员函数内使用this外,形参实例化地址和实参指针都不能自己补充,因为这是编译器的事情),然而这个this就是表示实例化对象地址。this是存储堆或者寄存器。
#include<iostream>
using namespace std;
class Date
{
public:
void Init(int year = 1, int month = 1, int day = 1)
//实际void Init(Date const* this,int year = 1,int month = 1,int day = 1)编译器自动补充,我们不能补充
{
_year = year;
_month = month;
_day = _year;
//在成员函数内部就能自己补充或者由编译器补充
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
cout << this->_year << "年" << this->_month << "月" << this->_day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2025, 8, 23);//等价于d1.Init(&d1,2025,8,23)
d1.Print();//等价于d1.Init(&d1,2025,23)不能自己补充
return 0;
}
这里还需要注意,当对类指针拷贝一个空指针时,使用该对象并不会导致空指针的引用。除非是在调用函数内部使用this指针会导致空指针的解引用。
#include<iostream>
using namespace std;
class Data
{
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()
{
Data* d1 = nullptr;
d1->Init(2025, 8, 10);//这里正常,没设计到空指针引用,这里是指的调用Init里面函数
(*d1).Init(2025, 8, 10);//这里同理
d1->Print();//会运行错误,空指针引用,函数内部使用了this->_year等空指针解引用
return 0;
}
二、类和对象的常用的六大基本默认成员函数
2.1 构造函数
2.1.1 定义和用法
他是一个特殊的成员函数,这是对象实例化定义时的初始化。构造函数的出现是为了避免程序员在写代码过程中忘记初始化。
#include<iostream>
using namespace std;
class A
{
public:
A(int a1 = 10, int a2 = 20)
{
_a1 = a1;
_a2 = a2;
}
void Init(int a1 = 10, int a2 = 20)
{
_a1 = a1;
_a2 = a2;
}
void Print()
{
cout << _a1 << _a2 << endl;
}
private:
//声明
int _a1;
int _a2;
};
int main()
{
A aa1(100,200);//直接调用默认构造函数(以及初始化)
aa1.Print();
//这里也会调用构造函数,只要是初始化就会调用构造函数(没有构造函数就会调用默认构造生成函数后面语法会将)
A aa2;
aa2.Print();
return 0;
}
2.1.2 特点(语法)
1.函数名与类名相同
2.无返回值
3.对象实例化时,系统会自动调用对应的构造函数
4.构造函数可以重载(对成员函数初始化的时候再定义实例化对象中输入实参即可,重载就是多了很多初始化方法,构造函数不止默认构造函数)。
5.如果类中没有显示定义构造函数,则c++编译器会自动生成一个默认构造函数(也就是默认生成构造函数),一旦用户显示定义,编译器则不会生成。
6.a.无参构造函数,b.全缺省构造函数和c.编译器默认生成的构造函数都叫默认构造函数。这三个构造函数只能存在一个,c和ab是互斥的,不写就是c,写了就是a或者b(语法上是支持a和b一起的,因为这构成了函数重载,但是在类中这会导致二义性出现,导致编译器不知调用哪个函数)。这里和构造函数和默认构造函数不是同一个东西,构造函数包含默认构造函数。
构造函数(Constructor):是一个总称,指的是一类特殊的成员函数,用于初始化类的对象。它包括所有形式的构造函数(无参的、有参的、拷贝的、移动的等)。
默认构造函数(Default Constructor):是构造函数的一个子集,特指那些可以不提供任何实参就能调用的构造函数。
7.如果我们不写,编译器默认生成构造直接对类实例化对象就行(随机值),而如果不想使用默认生成的,那就直接写一个构造函数然后再定义类实例化对象传参即可(这里如果不传,不用()因为这个会导致编译器的无法分清是声明还是调用)。还有自定义类类型成员变量,调用编译器默认生成构造,会寻找自定义类(对于自定义函数中自定义类型没有默认生成构造函数,但是后面会讲怎么解决)内出现的构造函数进行初始化。
#include<iostream>
#include<assert.h>
using namespace std;
class Stack
{
public:
Stack()//默认构造函数
{
_a = (int*)malloc(sizeof(int) * 4);
if (_a == NULL)
{
perror("malloc");
exit(1);
}
_capacity = _top = 0;
cout << "Stack初始化构造函数" << endl;
}
void StackPush(int x)
{
assert(_a);
//扩容
if (_capacity == _top)
{
_capacity = _capacity * 2;
int* tmp = (int*)realloc(_a, sizeof(int) * _capacity);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
_a = tmp;
}
_a[_top++] = x;
}
private:
int* _a;
int _capacity;
int _top;
};
class MyQueue
{
public:
private:
//自定义类型
Stack d1;
Stack d2;
};
int main()
{
MyQueue q1;
int a = 0;
cin >> a;
cout << a << endl;
return 0;
}
总结:构造函数是必须有的,就算不写它也会默认构造。大部分场景下需要自己写构造函数来管理自己成员变量。
2.2 析构函数
2.2.1 定义和用法
与构造函数相反,析构函数类似于destory完成对象中资源的清除释放工作(一般没有向内存申请资源的没必要析构,也不需要析构,析构是针对于那些向堆上申请空间和需要清理资源的对象),避免内存泄露,他不是对对象本身的销毁。这个析构函数是为了怕忘记释放内存而导致的内存泄露。
#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()
{
_year = 0;
_month = 0;
_day = 0;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
2.2.2 特点(语法)
1.析构函数名是在类名前面加个~字符。
2.无参数无返回值(和构造一样,不需要void)。
3.一个类中只能有一个析构函数(与构造函数区别,构造可以有重载),如果没定义,系统会自动生成默认析构函数。
4.对象生命周期结束,系统自动调用析构函数,也就是在return时调用(最大特点)。
5.和构造函数类似,不写编译器析构函数,会对内置类型(C++把类型分成内置类型(基本类型)和⾃定义类型。内置类型就是语⾔提供的原⽣数据类型, 如:int/char/double/指针等,⾃定义类型就是我们使⽤class/struct等关键字⾃⼰定义的类型)成员不做处理,自定义类型成员会调用他的析构(也就是一个类中出现自定义类型成员,他会在类中调用一次析构,然后在自定义类型成员里面又调用一次析构)。
#include<iostream>
#include<assert.h>
using namespace std;
class Stack
{
public:
Stack()//默认构造函数
{
_a = (int*)malloc(sizeof(int) * 4);
if (_a == NULL)
{
perror("malloc");
exit(1);
}
_capacity = _top = 0;
cout << "Stack初始化构造函数" << endl;
}
void StackPush(int x)
{
assert(_a);
//扩容
if (_capacity == _top)
{
_capacity = 4+_capacity * 2;
int* tmp = (int*)realloc(_a, sizeof(int) * _capacity);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
_a = tmp;
}
_a[_top++] = x;
}
~Stack()//析构函数(只能有一个,默认调用)
{
cout << "Stack销毁析构函数" << endl;
free(_a);
_a = NULL;
_capacity = _top = 0;
}
private:
int* _a;
int _capacity;
int _top;
};
class MyQueue
{
public:
private:
//自定义类型
Stack d1;
Stack d2;
};
int main()
{
MyQueue q1;
return 0;
}
这里实际上调用了3次析构,两次在自定义类中调用d1d2,还有一次在MyQueue这个类中调用一次默认析构函数。
6.一个局部域中有多个对象时,c++规定后定义的先析构。
2.3 拷贝构造函数
2.3.1 定义和用法
拷贝构造是一个特殊的构造函数,而拷贝构造指对类实例化时调用第一个参数为自身类类型名(也就是类的实例化对象名)的引用,且任何额外参数(这个参数可有可不有)都有默认值(如果有,必须得有默认值也就是缺省值)。这里也印证了构造函数可以重载这一特点。
#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;
}
void Print()
{
cout << _year <<"年" << _month <<"月" << _day <<"日" << endl;
}
~Date()
{
_year = 0;
_month = 0;
_day = 0;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025,8,23);//初始化
Date d2 = d1;//拷贝构造
Date d3(d2);//拷贝构造
d2.Print();
Date d3 = 3;//这也是拷贝构造,只不过进行了类型转换,后面会将
return 0;
}
2.3.2 特点(语法)
1.拷贝构造函数是构造函数的一个重载。
2.拷贝构造函数第一个参数必须是类类型对象的引用(指针也可以但是不叫拷贝构造,为普通构造。因为引用用的是别名,不会去拷贝,而每次调用拷贝函数之前要传值传参,也就是第三点特点会讲),使用传值会无限递归,它可以有多个参数,但是第一个必须是类对象,且后面参数必须要有缺省值。
图片上用地址的构造函数是因为写了一个传地址的构造函数(与拷贝构造就差的是传的参数9一个是指针一个是引用)。
3.在c++规定自定义类型(这里自定义类型指的是class或struct这些类组成的类型)对象进行拷贝必须调用拷贝构造函数。在函数传值传参时,在一个函数中传一个类对象参数,那么这个函数会先把参数类的函数先调用完,再回到当前函数进行使用(任何函数都是先使用参数前都会判断该参数是否为一个函数,如果该参数为一个函数,那么会先调用完再被原函数使用)。在传值返回也会调用拷贝构造(只不过在一些编译器中会优化)。
#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()
{
_year = 0;
_month = 0;
_day = 0;
}
private:
int _year;
int _month;
int _day;
};
void Func1(const Date d)
{
//...
}
Date Func2()
{
Date d1(2025,8,23);
return d1;
}
int main()
{
Date d1;
Func1(d1);//传值拷贝构造
Func2();//返回拷贝构造
return 0;
}
4.若未显示定义拷贝构造,编译器会自动生成拷贝构造函数。而该自动生成拷贝构造函数是浅拷贝。当遇到开辟空间需要深拷贝的需要自己去实现(避免一个数据的改变,从而改变原拷贝数据值,并且再调用析构函数时会调用两次导致的运行报错),自定义类型成员变量会调用自定义成员的拷贝构造(当然也离不开写拷贝构造函数,只不过是在自定义成员中类的拷贝构造)。可以总结为写析构函数,必须要写拷贝构造函数。
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
//构造函数
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
//拷贝构造(深拷贝)
// st2(st1)
Stack(const Stack& st)
{
_a = (STDataType*)malloc(sizeof(STDataType) * (st._capacity));
if (_a == nullptr)
{
perror("malloc fail");
return;
}
memcpy(_a, st._a, sizeof(STDataType) * st._top);
_top = st._top;
_capacity = st._capacity;
}
void Push(const STDataType& x)
{
if (_top == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : (2 * _capacity);
STDataType* newarr = (STDataType*)realloc(_a,newcapacity * sizeof(STDataType));
if (newarr == nullptr)
{
perror("malloc fail");
return;
}
_a = newarr;
_capacity = newcapacity;
}
_a[_top] = x;
_top++;
}
int Top()
{
return _a[_top-1];
}
//析构
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
class MyQueue
{
private:
Stack _pushst;
Stack _popst;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
st1.Push(3);
st1.Push(4);
//cout << st1.Top() << endl;
Stack st2=st1;//等价于Stack st2(st1);//拷贝构造函数
cout << st2.Top() << endl;
return 0;
}
像这种有资源的申请的就需要自己写深拷贝(拷贝构造函数)。这里会调用三次析构,如果不写深拷贝来申请独立空间,就会导致一个空间析构2次,从而运行错误。
5.像Date这样的类成员变量全是内置类型且没有指向什么资源(指向空间),编译器⾃动⽣成的拷⻉构造就可以完 成需要的拷⻉,所以不需要我们显⽰实现拷⻉构造。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器⾃动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的拷⻉构造会调⽤Stack的拷⻉构造,也不需要我们显⽰实现MyQueue的拷⻉构造。内置类型指得是int,int*数组这些没有开辟空间的类型。
6.传值放回中如果放回的是引用返回,对于返回一些类对象,函数销毁了会是该类对象也会销毁(也就是会调用析构函数),所以返回后为空引用会出问题。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在才能用引用返回。不过引用返回能够减少拷贝,在引用返回时,可以通过一个步骤比如调成静态来使得当前函数结束后能发现这个变量或者对象。
typedef int STDataType;
class Stack
{
public:
//构造函数
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
//拷贝构造(深拷贝)
// st2(st1)
Stack(const Stack& st)
{
_a = (STDataType*)malloc(sizeof(STDataType) * (st._capacity));
if (_a == nullptr)
{
perror("malloc fail");
return;
}
memcpy(_a, st._a, sizeof(STDataType) * st._top);
_top = st._top;
_capacity = st._capacity;
}
void Push(const STDataType& x)
{
if (_top == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : (2 * _capacity);
STDataType* newarr = (STDataType*)realloc(_a,newcapacity * sizeof(STDataType));
if (newarr == nullptr)
{
perror("malloc fail");
return;
}
_a = newarr;
_capacity = newcapacity;
}
_a[_top] = x;
_top++;
}
int Top()
{
return _a[_top-1];
}
//析构
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
class MyQueue
{
private:
Stack _pushst;
Stack _popst;
};
//Stack& Func()
//{
// Stack st;
// st.Push(1);
// st.Push(2);
// st.Push(3);
// st.Push(4);
// return st;//不会调用拷贝构造函数并且会返回空引用,这里return时st已经销毁了
//}
//解决办法
static Stack& Func()//关于这里函数前面加static是因为这里返回的是静态函数地址,后续会讲static
{
static Stack st;//设为静态函数,使得作用域放大避免销毁(使得返回时调用拷贝构造函数)
st.Push(1);
st.Push(2);
st.Push(3);
st.Push(4);
return st;//也会调用类中的拷贝构造
}
//方法二
//Stack Func()
//{
// Stack st;
// st.Push(1);
// st.Push(2);
// st.Push(3);
// st.Push(4);
// return st;//调用拷贝构造函数,但是被优化了
//}
int main()
{
Stack st1(Func());
cout << st1.Top() << endl;
return 0;
}
以下这张图反映了第一个(没静态函数),此时返回的是空引用。

2.4 运算符重载函数
2.4.1 定义和用法
运算符重载是由于类类型对象不能直接运算,而产生的一个运算符重载含义。一般可以直接调用operator,但是常见是直接用类进行运算就够了,编译器会自动调用,如d1+=100自动调用operator+=这个运算符重载函数(没有则会默认生成,默认生成的针对于赋值这些需求是浅拷贝),在函数内用一个操作符如果参数符合,也会自动调用其对应的运算符重载函数。
#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;
}
int Getmonthday(int year, int month)
{
static int monthday[13] = { 0,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;
}
return monthday[month];
}
//+=运算
Date& operator+=(int day)
{
_day += day;
while (_day > Getmonthday(_year, _month))
{
_day -= Getmonthday(_year, _month);
++_month;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
//析构
~Date()
{
_year = 0;
_month = 0;
_day = 0;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 8, 24);
d1.Print();
d1 += 100;//也可以是d1.operator+=(100);
d1.Print();
return 0;
}
2.4.2 规则(语法)
规则1:当对类类型对象使用运算符时,编译器会尝试查找合适的运算符重载函数。如果没有找到合适的重载,编译器会尝试使用内置运算符(如果操作数可以转换为适用内置运算符的类型)。如果两者都不可用,则会导致编译错误。
规则2:运算符重载是由一个关键字operator和后面要定义的运算符共同构成,和其他函数一样也具有返回类型(根据具体情况,日期加加是返回日期,那就返回什么类型,还有比较大小时返回bool类型)和参数列表。
规则3:重载运算符函数与的参数个数与运算符作用运算个数一样多(也就是一元运算符对于一个数如++,二元运算符+、-、=这一类需要两个操作数为二元运算符,归根结底就是运算符需要几个操作数就参数个数需要几个)。注意这里左边运算对象传第一个参数,右边运算对象传第二个参数。
规则4:如果一个运算符重载函数是成员函数,由于第一个默认参数是this,所以再写函数时,会少写一个参数
规则5:不能通过语法中没有的符号进行重载。
规则6:重载操作符至少要有一个类类型(不能用于参数内全是内置类型的)。
规则7:重载要符合实际意义上(满足自己的需求,不能日期去用乘这个符号重载,没意义)。
规则8:.* :: sizeof ?: . 这些不能重载。
规则9:对于前置++和后置++,运算符重载都是operator++,无法很好区分,但是c++规定后置++重载,参数增加一个int形参与前置++进行区分。当调用显性调用时,operator++()时用后置加加参数可以是随意的数(这里只要表示传形参就能说明是调用后置++)。前置加加返回的加加后修改的值,而后置加加返回的是加加前的值(本质都是对原类*this进行++,前置++返回的引用也就是没有默认构造,而是直接对*this++并返回*this,而后置++返回的是函数内默认构造的类,这个类并没有改变,但是*this改变)。
规则10:
这个规则和规则三有关系,由于cin和cout的流入流出运算符顺序是out和in为左运算数,而类是右运算符,所以这里不能把<<或>>运算符重载写入类中,这时候就需要友元来和使用类中私有成员变量/函数。
#include<iostream>
using namespace std;
class Date
{
//声明
friend ostream& operator <<(ostream& out, const Date& d);
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;
}
int Getmonthday(int year, int month)
{
static int monthday[13] = { 0,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;
}
return monthday[month];
}
//+=运算
Date& operator+=(int day)
{
_day += day;
while (_day > Getmonthday(_year, _month))
{
_day -= Getmonthday(_year, _month);
++_month;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
//析构
~Date()
{
_year = 0;
_month = 0;
_day = 0;
}
private:
int _year;
int _month;
int _day;
};
ostream& operator <<(ostream& out, const Date& d)//这里ostream是c++标准输出流类(在头文件ostream)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
int main()
{
Date d1(2025, 8, 24);
cout << d1;
d1 += 100;
cout << d1;
return 0;
}
2.4.3 赋值运算符重载
2.4.3.1 用法
调用时可以选择直接调用operator或者直接将一个类赋值(这是最常见的,直接如d1=d2会自动调用operator),正是因为这个它和构造函数有点类似,这里可以这样区分它如果是一个类在实例化对象时如date d2=d1,这是调用构造函数(拷贝),而对呀d1=d3已经实例化对象d1,等于一个类就是赋值运算重载,会默认调用赋值运算重载。
#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& operator =(const Date& d)//对于默认生成的是浅拷贝,在有资源的成员中需要自己写深拷贝
{
_day = d._day;
_month = d._month;
_year = d._year;
}
//析构
~Date()
{
_year = 0;
_month = 0;
_day = 0;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 8, 24);
Date d2 = d1;//构造函数(拷贝)
Date d3;
d3 = d1;//调用赋值运算符重载
d3.operator=(d1);//这也可以
return 0;
}
2.4.3.2 特点
1.重载在成员函数会更好,建议在形参中加入const和引用修饰,避免后面代码运算符写错导致实现和预期不符合,同时避免了在传参过程中的权限放大和调用拷贝构造函数对性能效率的影响。
2.有返回值,建议写成引用返回返回减少拷贝,也方便连续赋值(没引用会拷贝临时变量不能连续赋值),同时为了避免没意义相同类和对象的调用使用,这里需要判断。
Date& operator=(const Date& d)
{
//为了不必要的拷贝
if(*this != d)//当然这里也需要调用运算符重载
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;//传引用可以避免拷贝带来的效率影响
}
3.和以上特殊函数一样,没有显示实现,编译器会自动生成一个默认赋值运算符重载,对于自定义类型也会调用它的赋值重载函数。像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的赋值运算符重载就可以完成需要的拷⻉,所以不需要我们显⽰实现赋值运算符重载。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器⾃动⽣成的赋值运算符重载完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的赋值运算符重载会调⽤Stack的赋值运算符重载,也不需要我们显⽰实现MyQueue的赋值运算符重载。这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就需要显⽰写赋值运算符重载,否则就不需要。(注:当MyQueue类中没有实现运算符重载,而Stack类中使用了运算符重载,对MyQueue的对象调用运算符重载是不会调用Stack中类的运算符重载的)
#include <iostream>
using namespace std;
typedef int STDataType;
// Stack 类实现了运算符重载
class Stack {
private:
STDataType* _a;
size_t _capacity;
size_t _top;
public:
public:
//构造函数
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
//拷贝构造(深拷贝)
// st2(st1)
Stack(const Stack& st)
{
_a = (STDataType*)malloc(sizeof(STDataType) * (st._capacity));
if (_a == nullptr)
{
perror("malloc fail");
return;
}
memcpy(_a, st._a, sizeof(STDataType) * st._top);
_top = st._top;
_capacity = st._capacity;
}
void Push(const STDataType& x)
{
if (_top == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : (2 * _capacity);
STDataType* newarr = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType));
if (newarr == nullptr)
{
perror("malloc fail");
return;
}
_a = newarr;
_capacity = newcapacity;
}
_a[_top] = x;
_top++;
}
int Top()
{
return _a[_top - 1];
}
// Stack 类实现了 + 运算符重载
Stack operator+(const Stack& other) const
{
//...
}
};
// MyQueue 类没有实现任何运算符重载
class MyQueue {
private:
Stack stack1;
Stack stack2;
public:
// 注意:MyQueue 没有实现任何运算符重载
};
int main() {
// 创建两个 Stack 对象并测试运算符重载
Stack s1, s2;
s1.Push(1);
s1.Push(2);
s2.Push(3);
s2.Push(4);
// Stack 对象可以使用 + 运算符
Stack s3 = s1 + s2;
// 创建两个 MyQueue 对象
MyQueue q1, q2;
// 尝试对 MyQueue 对象使用 + 运算符
// 下面的代码会导致编译错误,因为 MyQueue 没有实现运算符重载
// MyQueue q3 = q1 + q2; // 错误:没有匹配的运算符
// 尝试对 MyQueue 对象使用输出运算符
// 下面的代码也会导致编译错误
// cout << q1 << endl; // 错误:没有匹配的运算符
cout << "程序结束" << endl;
return 0;
}
2.4.4 不同代码-和-=运算符区别(拷贝次数)
第一张图代码:右侧减运算符重载是d1-100(这里是因为-运算不改变原类中的值,所以需要拷贝构造一个,然后调用左侧-=来修改拷贝的值并返回)进行2次拷贝。然后左侧减等运算符重载d2-=100;这里并没有拷贝。因此总共d1-100和d2-=100一起调用2次拷贝。

第二张图代码:右侧减号运算符重载d1-100调用相当于第一张图代码合并(这不用调用-=运算符重载),只拷贝2次构造。左侧减等运算符重载d2-=100,调用减运算符重载,调用2次构造,所以总共合计d1-100和d2-=100代码一起是调用4次构造

这说明将不同运算符重载分开写是有利于减少拷贝构造的,让他们独立实现功能,不建议把他们写在一个运算符重载中,避免拷贝构造对性能的影响。
#include<iostream>
using namespace std;
class Date
{
public:
//构造
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
//拷贝
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
int Getmonthday(int year, int month)
{
static int monthday[13] = { 0,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;
}
return monthday[month];
}
//-=运算符重载
Date& operator-=(int day)
{
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += Getmonthday(_year, _month - 1);
}
return *this;
}
//前置--运算符重载
Date& operator--()
{
*this -= 1;
return *this;
}
//后置--运算符重载
Date operator--(int)
{
Date d1(*this);
d1 -= 1;
return d1;
}
//析构
~Date()
{
//...
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 8, 24);
--d1;
d1--;
return 0;
}
一般来说我们在使用前置--和后置--都能实现一个功能时,我们会首先选择前置--,因为前置--没有拷贝构造,而后置--有2次拷贝构造(算返回优化前)。
2.5 const成员函数
2.5.1 定义和用法
在实例化对象时加入const,调用函数时会传const Date* const d(const Date d(2025.2.28)这个实例化对象加const)但是接受函数对象(类里面定义)编译器默认并无法在形参修改,因为这是编译器自己的事情,默认为Date* const d(以上面date为例),所以传参会导致权限放大。
class Date
{
public:
void Print() const //void Print (Date* const d)编译器默认补充所以我们无法在内部补充const加上c++没位置加const只能在函数名加const进行对默认补充的类对象进行限制
{
//...
}
private:
int _year;
int _month;
int _day;
};
int main()
{
const Date d1;
d1.Print();
return 0;
}
解决方式是在函数定义和声明后面加const(c++函数前面没地方放const,所以规定在定义和声明参数后面加)。
总结:只要不改变调用对象成员变量的函数都加const.。(解释一下,在类中的函数中默认date* const 限制该对象的指向不被修改(地址不能变),而上面类中加的const指的限制了对象的内容不被修改加上默认的指向不被修改)
2.6 取地址运算符函数重载(参数不同函数名相同)
2.6.1 定义和用法
分为普通取地址运算符重载和const取地址运算符重载,编译器会默认生成,也可以自己写,在调用时编译器会匹配最适合的,比如const匹配const 普通的匹配普通的而不是去匹配const。
#include<iostream>
using namespace std;
class Date
{
public:
//取地址重载
Date* operator&()//和下面那个不构成重载,只有一个即可
{
return this;
}
//const取地址重载
//const Date* operator&()
//{
// return this;
//}
const Date* operator&()const//是上面的重载函数
{
//这里可以诱骗给假地址
return this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
////Date* operator&()
Date d1;
Date* pd1 = &d1;
//const Date* operator&()或者const Date* operator&()const只不过这个进行了权限缩小
Date d2;
const Date* pd2 = &d2;
//const Date* operator&()只能这个不能权限放大
const Date d3;
const Date* pd3 = &d3;
return 0;
}
如果不想要让别人取到当前类对象的地址,就可以自己实现一份,然后随便返回一个地址(诱骗)。
三、类和对象的其他语法
3.1 再探函数构造
3.1.1 定义用法
再探构造函数是一个构造函数初始化的另一种方式为初始化列表,一般实际上使用它来当构造函数比较好用,既可以写默认构造函数也能写默认构造不能写的(也就是必须初始化的,后面规则会将)。
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
//初始化(只能初始化一次)
:_year(year)
,_month(month)
,_day(day)
{
//赋值(赋值多次)
_year = year;
_year = 2;
}
private:
int _year;
int _month;
int _day;
};
3.1.2 规则(语法)
1.使用方式第一个以冒号开始然后进行初始化成员变量初始化之间用逗号分开。
2.每个成员变量在初始化只能出现一次(不像默认构造函数可以出现很多次)。可以理解成初始化列表主要是定义成员变量初始化的地方,一个变量不能两次初始化(为第三个解释说明必须初始化列表的成员变量)。
3.对于一些const修饰成员变量、引用成员变量和没有默认构造的类类型变量必须使用初始化列表初始化(因为这几个成员变量在开辟空间后必须给予初始化)
#include<iostream>
using namespace std;
class Test
{
public:
//不是构造函数(成员变量没赋值也没写全,加上这里显现了构造阵形不会调用默认构造生成函数)
//没写正确的构造函数
Test(int aa)
{
cout << "Test(int aa)" << endl;
}
private:
int a;
int b;
int c;
};
class A
{
public:
A(int a,int& b)
:_a(a)
,_b(b)
,_c(1)
{
}
private:
//声明
const int _a;
int& _b;
Test _c;
};
4.初始化列表按声明进行初始化定义,和初始化列表内部顺序没关系。当遇到没有初始化列表没有对声明的定义时,1.它会先去看是否有缺省参数(这里的缺省参数是指在类中声明的成员变量直接赋值,对于指针变量可以申请空间来表示默认值),2.如果内置类型没有缺省值则会给一个随机值或者0(这里显示是指实例化过程初始化,因为实例化过程中就已经对内部分批了空间)。如果类置类型没有缺省值则会调用默认构造函数,没有默认构造函数会报错。
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
/*Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}*/
//初始化列表
Stack(int n = 4)
//最好按照声明顺序定义
:_a((STDataType*)malloc(sizeof(int)* n))
,_capacity(4)
,_top(0)
{
//上面写定义
//这里可以用来写判为空条件
if (_a == NULL)
{
perror("malloc fail");
return;
}
}
// st2(st1)
Stack(const Stack& s)
{
cout << "Stack(Stack& s)" << endl;
_a = (STDataType*)malloc(sizeof(STDataType) * s._capacity);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
memcpy(_a, s._a, sizeof(STDataType) * s._top);
_top = s._top;
_capacity = s._capacity;
}
void Push(const STDataType& x)
{
// 扩容
_a[_top] = x;
_top++;
}
int Top()
{
return _a[_top-1];
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
class MyQueue
{
public:
//它具备构造函数的特征(可以写可不写,但是成员必须写构造)
MyQueue(int n = 4)
:_pushst(n)
, _popst(n)
,_size(0)
{
}
private:
// 缺省值
Stack _pushst;
Stack _popst;
int _size = 4;
};
int main()
{
MyQueue q1;
return 0;
}
总结:每个构造函数都有初始化列表,所以每次初始化都会调用初始化列表(默认也可以自己写,所以一般我们写再探构造函数就行了,不用写默认构造函数)。
3.2 类的类型转换规则
1.c++内置类型隐式转换为类类型对象,这个前提类里面必须有内置类型参数的构造函数(不管是内置类型转换为类置类型,还是类置类型转换为另一个类置类型,都需要调用关于需要改变之前转换类型的参数的构造函数)。
#include<iostream>
using namespace std;
class A
{
public:
A(int a1 = 1, int a2 = 2)
:_a1(a1)
,_a2(a2)
{
}
A(const A& aa)
{
//...
}
void Test(const A& aa)
{
//...
}
private:
int _a1;
int _a2;
};
int main()
{
A aa1 = 1;//隐式类型转换连续构造加拷贝构造->编译器直接优化直接执行构造函数
A aa2 = { 10,20 };//多参数隐式类型转换,这里是初始化列表隐式转换
A aa3{ 100,200 };//初始化列表和A aa3(100,200)一样
A aa4 = (1000, 2000);//这是逗号表达式和A aa4 = 1000;表达的意思一样虽然也是隐式类型转换
A aa5;
aa5.Test(2);//也是类型转换,先对2进行类型转换构造为A类型,然后再使用
return 0;
}
这里连续构造是转化构造,普通构造,默认构造,连续构造+拷贝构造会优化为直接构造,这也就是类型转换能提高性能。
2.对于不想支持隐式类型转换就会在构造函数前面使用explicit。
class A
{
public:
//使用explicit取消隐式类型转换
explicit A(int a1 = 1, int a2 = 2)
:_a1(a1)
,_a2(a2)
{
}
private:
int _a1;
int _a2;
};

3.对于多参数类型转换要使用{}括起来
4.对于类与类之间的隐式转换也是通过对应构造函数转换(对应的就是构造函数里面写关于类的构造)。
总结:对于未初始化的类对象进行初始化时,有等号就会发生类型转换,没等号(也就是用括号初始化的)那就是普通的构造初始化。对于调用某个函数中参数为类对象,调用时传内置类型或者初始化列表(在()内加入花括号,进行多参数初始化)也会发生类型转换,一个是隐式类型转换,一个是初始化列表转换(通过学习得到初始化会更安全一点,不收explicit影响)
3.3 类中的static静态成员
static静态(static不管在哪都是在编译过程就已经初始化了)
规则1:static声明的成员变量,不会和其他内置类型或者自定义类一样属于某个对象,static是属于当前类的所有对象,所以不能在声明处加缺省值,必须在类外面初始化。
规则2:static修饰的成员函数是静态成员函数,静态成员函数没有this指针。不过他能访问静态成员,因为静态成员函数没this指针不能访问。
规则3:非静态成员函数可以访问静态成员变量和静态成员函数,可以突破域去调用静态成员变量。
规则4:静态成员也是类的成员也受限定符的限制(这里静态成员包括静态成员函数和静态成员变量)。
#include<iostream>
using namespace std;
class A
{
public:
static int Get_a1()//里面编译器没有参数
{
return _a1;
}
private:
static int _a1;
int _a2;
};
int A::_a1 = 100;//初始化
int main()
{
A aa1;
//aa1._a1;//错误(受类中private限制)
A::Get_a1();//成员函数返回静态变量时必须使用静态函数返回(严谨)
//int a = aa1.Get_a1();//相较于类名调用宽松
return 0;
}
特殊点:在调用静态函数时,可以用对象去引用函数,当然也可以直接类类名调用该函数,一般我们会直接使用类名调用静态函数(会更加严谨,需要加static修饰函数),而这里对象引用该函数可以不用static修饰函数,不够严谨。
更正图中一个小问题:这里通过对象调用的关于空指针安全性,对象引用时得分情况来看是否空指针是解引用,又使this指针后面没用不影响(或者用static修饰,不使用this指针)
3.4 类中的友元
友元用于突破限定符分装的方式,在类(可以任意放)中声明时前面加friend,可以在类外使用私有成员。不经如此一个函数还可以是多个类的友元函数,需要在各个类中声明对应的友元函数(每个类友元声明后,这个还需要声明最后一个定义的类,不然编译器会报错识别不到最后一个类),如果该函数一个函数就不用声明另一个类,只需要声明友元即可。
#include<iostream>
using namespace std;
class B;//声明
class A
{
friend void Test(const A& aa, const B& bb);
//...
};
class B
{
friend void Test(const A& aa, const B& bb);
//...
};
void Test(const A& aa, const B& bb)
{
//使用A和B的私有成员
}
int main()
{
A aa1;
B bb1;
Test(aa1, bb1);
return 0;
}
类直接也可以友元,在A类中声明友元B类,B是A的友元,但是A不是B的友元,这是单向的可以在B类中使用A中的私有,但是A中私有不能被B中调用。友元关系不能传递,A类是B类的友元,B类是C类友元,但是A不是C类的友元,可以将这里友元理解成朋友请客去家里玩(具体说:A请B来家里玩,B可以随便使用一些东西,B请C来家里玩,C可以随便使用东西,但是A不会请C来家里玩)。
class A
{
friend class B;//可以使得B使用A中的私有成员
public:
//..
private:
//..
};
class B
{
//...
};
友元破坏了封装,增加耦合度,一般不会多用。
3.5 内部类(友元的区别)
3.5.1 用法
一般的类设置在全局域,而在类部中设置类称为内部类。内部域是一个独立的类,跟定义在全局域中类比,他只是收外部类类域的限制和访问限定符影响,没有其他左右,也不包含在外部类对象中(所以一般计算外部域大小不会计算内部域)。内部类默认是外部类的友元(内部类是外部类的朋友,内部类可以随便使用外部类的东西)。
class A
{
public:
class B//(不被计算再A类空间大小)
{
public:
//可以访问A中的私有成员
private:
};
private:
};
3.5.2 内部类的应用
class Solution {
// 内部类
class Sum
{
public:
Sum()
{
_ret += _i;
++_i;
}
};
static int _i;
static int _ret;
public:
int Sum_Solution(int n) {
// 变⻓数组
Sum arr[n];
return _ret;
}
};
int Solution::_i = 1;
int Solution::_ret = 0;
这里就充分利用了内部域,仅供内部使用,避免外部其他使用。
3.5.3 与友元的区别
友元和内部类看起来差不多,但是他们是正交关系也就是类似于数学中的相互独立。
3.6 匿名对象
匿名对象生命周期只在当前一行,和临时对象类似。
#include<iostream>
using namespace std;
class A
{
public:
void Print()
{
cout << _a1 << _a2 << endl;
}
private:
int _a1;
int _a2;
};
int main()
{
A().Print();//生命周期为当前这一行
return 0;
}
四、总结
在学习过程中,我认为比较重要的点激素那6大默认构造函数,其中6大默认构造函数中也有重要的(也就是重中之重)就是默认构造函数、析构函数、拷贝构造函数和运算符重载函数,其中有些还涉及到了权限放大缩小这些,权限放大是很严重的问题。然后就是类型转换的理解也很重要(包括优化这些),其次就算友元和内部域的区别。总的来说就是理解类和对象中的使用和应用。
总结一下使用私有成员的用法
对于类外不能使用类内成员变量(private)当需要使用类内成员函数时有三种方法:
1在类中提供一个getxxx函数(自己在成员函数中定义一个函数,然后即可在类外调用)。
2.友元
3.直接放到类里面(只不过放到类里面后参数会变多一个,而那个参数是this对应的类对象参数地址,优先考虑这个方法)。
1454

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



