C++第二讲:类和对象
1.类的定义
1.1类定义格式
与结构体的定义格式相似,类定义的格式如下:
//类定义格式
//class关键字 + 类的名字
class Stack
{
};
但是与结构体不同的是,类中既可以定义变量,称为类的属性或成员变量,又可以定义函数,称为类的方法或成员函数,但是为了区分成员变量,一般在类的成员变量前面加上_或者以m(member)开头,这只是一个惯例,并不是强制要求的:
class Stack
{
// 成员函数
void Init(int capacity = 4)
{
_array = (int*)malloc(sizeof(int) * capacity);
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
//这里就是防止capacity分不清楚而加上_的示例
_capacity = capacity;
_top = 0;
}
void Push(int x)
{
// ...扩容
_array[_top++] = x;
}
int Top()
{
assert(_top > 0);
return _array[_top - 1];
}
void Destroy()
{
free(_array);
_array = nullptr;
_top = _capacity = 0;
}
// 成员变量
int* _array;
size_t _capacity;
size_t _top;
};//这里的分号不能省略
但是这时我们发现了错误:
这是为什么呢?我们往下看:
1.2访问限定符
C++中规定了三个访问限定符,规定了对象的访问权限,public修饰的成员可以在类外被直接访问,protected和private修饰的成员在类外不能被直接访问,它们两个的关系需要后序学习才能够看地更加透彻,访问操作符的使用方法如下:
class Stack
{
//直接在函数/变量最上边写上关键字即可
public:
void Init(int capacity = 4)
{
_array = (int*)malloc(sizeof(int) * capacity);
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_capacity = capacity;
_top = 0;
}
private:
// 成员变量
int* _array;
size_t _capacity;
size_t _top;
};//这里的分号不能省略
int main()
{
Stack st;
//此时,对于public修饰的成员,就可以直接在类外进行访问了:
st.Init();
//对于private修饰的成员,在类外不能被访问
st._top;//err:无法访问private成员
return 0;
}
1.3类域
C++中有函数局部域、命名空间域、全局域,而类定义了一个新的类域,不同的类域中可以定义相同名称的变量,而不同类域下的函数/变量访问都需要加上::作用符来指定域空间:
//假设我们要实现一个栈,此时我们要将栈的实现包装成三个文件:
//Stack.h Stack.cpp test.cpp
//Stack.h中:
class Stack
{
//成员函数:
public:
void Init(int capacity = 4);
//成员变量:
private:
int* array;
int capacity;
int top;
};
//Stack.cpp中:
//当函数需要很大的代码量时,需要实现声明和定义分离,这时就要指定类域
void Stack::Init(int n)
{
//此时能够访问成员变量
array = (int*)malloc(sizeof(int) * n);
if (nullptr == array)
{
perror("malloc申请空间失败");
return;
}
capacity = n;
top = 0;
}
//test.c中:
//没有出现错误
int main()
{
Stack st;
st.Init();
return 0;
}
1.4类定义注意事项
1.类中定义的函数默认为inline
2.C++中struct也可以定义类,其中也可以定义函数,不仅保留了C语言中的结构体的使用,还对结构体进行了升级,但是推荐使用class
3.class定义的成员没有访问限定符修饰时默认为private,struct默认为public
//struct定义类
struct Stack
{
//这里默认为public修饰
void Init(int n = 4)
{
array = (int*)malloc(sizeof(int) * n);
if (nullptr == array)
{
perror("malloc申请空间失败");
return;
}
capacity = n;
top = 0;
}
int* array;
size_t capacity;
size_t top;
};
int main()
{
Stack st;
st.Init();
st.top;//√,因为为public类型
return 0;
}
2.实例化
2.1什么是实例化
用类类型在物理内存中创建对象的过程,称为类的实例化出对象
类只是限定了类中有哪些成员变量,这些成员变量只是声明,并没有分配内存空间,只有听过类实例化出对象之后,才会分配空间:
//类的实例化
//比如我们定义了一个日期类,实现的是打印日期的功能:
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这个变量之后,才开辟了空间
//这一步称为类的实例化出对象,才能够使用d1中的变量
Date d1;
Date d2;
d1.Init(2024, 3, 31);
d1.Print();
d2.Init(2024, 7, 5);
d2.Print();
return 0;
}
就像是使用图纸造房子一样,类就是一张图纸,可以通过图纸造出很多房子,只有造出房子之后才能够在房子内部放置东西,并不能在图纸中放东西
2.2对象大小
类实例化出对象之后才会为对象分配内存空间,那么这一块内存空间是多少呢?
假设此时我们要计算下面三串代码的大小:
class A
{
public:
void Print()
{
cout << _ch << endl;
}
private:
char _ch;
int _i;
};
class B
{
public:
void Print()
{
//...
}
};
class C
{
};
补充:对于类来说,类实例化出对象之后只为成员变量开辟空间,并不会为成员函数开辟空间,因为函数被编译之后就是一串指令,无法进行存储,只能存储函数指针,但是对于指针的存储是不必要的,因为每一个对象调用的都是相同的函数代码,不需要每一次产生对象时都将函数指针进行存储,这只会时纯粹的空间浪费
对于A,涉及到内存对齐,之前已经讲过,这里只对对齐规则进行回忆,不再仔细阐述:
不难计算出,A的对象大小为8byte
B和C中都没有存储任何成员变量,那大小是否是0呢?不是,大小是1,所开辟的一个字节大小是为了占位使用,如果不开辟任何空间的话,对于类的&是无意义的,所以编译器会开辟1字节的空间表示占位标识对象存在
补充,为什么内存对齐让计算机访问效率更高?
处理器并不是一个字节一个字节进行读取的,而是多个字节进行读取的,当内存对齐为4时,处理器就可能被设置为4个字节进行一次读取,可以提高缓存的命中率,也不需要进行额外的内存访问获取剩余的字节
2.3this指针
既然上边说了,每一个对象调用相同的函数指令,那么如何确定到底是哪一个对象调用的函数呢?其实,每一个成员函数中,都存在着一个隐式的形参:this指针,它指向的是对象的地址:
//this指针
class Date
{
public:
//其实这里的函数原型为:
//void Init(Date const* this, int year, int month, int day)
void Init(int year, int month, int day)
{
//既然传入了指针,就要使用,那么这里实际上为:
//this->_year = year
_year = year;
_month = month;
_day = day;
}
//void Print(Date const* this)
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
//所以这里传入的实参实际上为:
//d1.Init(&d1, 2024, 3, 31)
d1.Init(2024, 3, 31);
d1.Print();
d2.Init(2024, 7, 5);
d2.Print();
return 0;
}
但是需要注意的时是,this指针是隐式的,会自动被编译器处理,不能够传入实参会自己定义this形参,但是能够在类函数中使用this指针:
//this指针
class Date
{
public:
void Init(Date const* this, int year, int month, int day)//err,语法错误
{
this->_year = year//√,可以使用this指针
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(&d1,2024, 3, 31);//err
return 0;
}
2.4题目练习
下面我们看两道题目,检查自己的学习成果,这里答案就直接展示了:
//1.下面程序编译运行结果为;
//A、编译报错
//B、运行崩溃
//C、正常运行
class A
{
public:
void Print()
{
cout << "A::Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
//这里这时对空指针进行传参,并没有对空指针进行解引用,所以程序正常运行,C
p->Print();
return 0;
}
//2.下面程序编译运行结果为;
//A、编译报错
//B、运行崩溃
//C、正常运行
class A
{
public:
void Print()
{
cout << "A::Print()" << endl;
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
//p->_a,对空指针进行了解引用,程序会运行崩溃,B
p->Print();
return 0;
}
因为this指针属于形参,所以存在于内存中的栈区
3.C语言和C++实现Stack对比
面向对象的三大特征:封装、继承、多态,下面的对比可以初步了解一下封装:
1.C++将数据和函数都封装到了类里边,通过访问限定符进行了限制,不能够随意通过对象直接修改数据,这个要求更加严格
2.C++中有⼀些相对⽅便的语法,⽐如Init给的缺省参数会⽅便很多,成员函数每次不需要传对象地址,因为this指针隐含的传递了,⽅便了很多,使⽤类型不再需要typedef⽤类名就很⽅便
#include <assert.h>
//假设我们此时要实现一个栈
typedef int STDataType;
class Stack
{
public:
void Pop()
{
assert(_top > 0);
--_top;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
int main()
{
Stack st;
//C++中,想要在这里访问top元素是不行的,而在C语言中,访问结构体中的元素是可行的
st._top++;
return 0;
}
4.构造函数
4.1什么是构造函数
构造函数并不是像它的名字说的那样构造出一个函数,为函数创建栈帧空间,而是在对象实例化时初始化对象,通过下面的使用我们就能够很好地理解这句话
4.2构造函数的使用
构造函数的使用特点总结:
1.函数名与类名相同
2.构造函数没有返回值,也不需要加void代表没有返回值,这是规定
3.对象实例化时会自动调用构造函数
4.构造函数可以重载
5.如果类中没有显式定义构造函数,那么编译器会自动生成一个无参的构造函数,一旦用户显式定义编译器就不再生成
6.默认构造函数并不是只有编译器默认生成的构造函数,它分为三种:无参构造函数、全缺省构造函数、以及编译器默认生成的构造函数,然而这三种构造函数并不能同时存在,因为无参构造函数和全缺省构造函数虽然构成函数重载,但是调用会存在歧义
7.编译器默认生成的构造函数对于内置类型(int、 char)的初始化是不确定的。对于自定义的成员变量(我们自己定义的,比如栈、队列等),需要初始化(C语言中使用Init函数进行了初始化),如果成员变量的类型没有默认构造函数,编译器无法完成它的初始化,编译器就会报错,这时要用初始化列表才能解决这个问题,这个下面再讲
1.函数名与类名相同
2.没有返回值,不需要加void
class Date
{
public:
//构造函数的使用
//1.函数名与类名相同
//2.没有返回值,不需要加void
Date()
{
cout << "Date()" << endl;
}
private:
int _year;
int _mouth;
int _day;
};
3.对象实例化时会自动调用构造函数
class Date
{
public:
//构造函数的使用
//1.函数名与类名相同
Date()
{
cout << "Date()" << endl;
}
private:
int _year;
int _mouth;
int _day;
};
int main()
{
//3.对象实例化,此时会自动调用构造函数Date
Date d1;
//在屏幕上会打印出Date(),正是构造函数中的打印
return 0;
}
4.构造函数可以重载,一般使用的都是全缺省构造函数,这样忘记传参或不需要传参时也不会出现错误
class Date
{
public:
//构造函数的使用
//1.函数名与类名相同
//2.没有返回值,不需要加void
//4.构造函数可以重载
//4.1 无参构造函数
Date()
{
cout << "Date()" << endl;
}
//4.2 带参构造函数
Date(int year, int mouth, int day)
{
cout << "Date(int year, int mouth, int day)" << endl;
}
//4.3 全缺省构造函数
Date(int year = 1, int mouht = 1, int day = 1)
{
cout << "Date(int year = 1, int mouht = 1, int day = 1)" << endl;
}
private:
int _year;
int _mouth;
int _day;
};
5.如果类中没有显式定义构造函数,那么编译器会自动生成一个无参的构造函数,一旦用户显式定义编译器就不再生成
class Date
{
public:
//5.此时类中没有定义构造函数,但是在运行时编译器其实会自动生成一个无参的构造函数
// 然后就会运行编译器提供的构造函数
private:
int _year;
int _mouth;
int _day;
};
int main()
{
Date d1;
return 0;
}
6.默认构造函数并不是只有编译器默认生成的构造函数,它分为三种:无参构造函数、全缺省构造函数、以及编译器默认生成的构造函数,然而这三种构造函数并不能同时存在,因为无参构造函数和全缺省构造函数虽然构成函数重载,但是调用会存在歧义
class Date
{
public:
//6.三种默认构造函数不能同时存在
//4.1 无参构造函数
Date()
{
cout << "Date()" << endl;
}
//4.3 全缺省构造函数
Date(int year = 1, int mouht = 1, int day = 1)
{
cout << "Date(int year = 1, int mouht = 1, int day = 1)" << endl;
}
private:
int _year;
int _mouth;
int _day;
};
int main()
{
//构造函数传参使用:
//Date d1(2024, 8, 19);
//6.当这样实例化出对象时,编译器就不知道要调用哪一个构造函数,存在歧义
Date d1();
return 0;
}
7.编译器默认生成的构造函数对于内置类型(int、 char)的初始化是不确定的。对于自定义的成员变量(我们自己定义的,比如栈、队列等),需要初始化(C语言中使用Init函数进行了初始化),如果成员变量的类型没有默认构造函数,编译器无法完成它的初始化,编译器就会报错,这时要用初始化列表才能解决这个问题,这个下面再讲
5.析构函数
5.1什么是析构函数
析构函数与构造函数的功能相反,构造函数主要是处理对象的初始化过程,而析构函数主要是完成对对象栈帧空间的销毁(free),类比之前栈的Destory功能
5.2析构函数的使用
析构函数的使用特点总结:
1.析构函数是在类名前加上~
2.没有返回值,没有参数,这是规定
3.一个类只能有一个析构函数,如果没有显示定义析构函数,那么编译器也会生成一个默认析构函数来使用
4.在对象的声明周期结束后,会自动调用析构函数,这样对于开辟的空间,就会自动释放,不需要手动释放
5.只要是使用了类实例化出了对象,就会调用它们的析构函数
6.如果类中并没有申请资源,析构函数就可以不写,因为没有空间需要释放,就可以不写,也可以写,但是一旦涉及到了资源的申请,就需要写析构函数
7.C++规定,后定义的先析构
1.析构函数是在类名前加上~
2.没有返回值,没有参数,这是规定
3.一个类只能有一个析构函数,如果没有显示定义析构函数,那么编译器也会生成一个默认析构函数来使用
typedef int STDataType;
class Stack
{
public:
//析构函数:
//1.析构函数是在类名前加上~
//2.没有返回值,没有参数,这是规定
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
4.在对象的声明周期结束后,会自动调用析构函数,这样对于开辟的空间,就会自动释放,不需要手动释放
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()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
int main()
{
Stack s1;
//4.main函数结束之后,s1对象的声明周期结束,这时会自动调用析构函数
// 会在屏幕上打印出~Stack()
return 0;
}
5.只要是使用了类实例化出了对象,就会调用它们的析构函数
// 两个Stack实现队列
class MyQueue
{
public:
//5.编译器默认⽣成MyQueue的析构函数调⽤了Stack的析构,释放的Stack内部的资源
// 显⽰写析构,也会⾃动调⽤Stack的析构
//~MyQueue()
//{
//
//}
private:
Stack pushst;
Stack popst;
};
int main()
{
Stack st;
MyQueue mq;
//5.这里会调用三次析构函数,因为MyQueue实例化出对象中,也实例化出了两个Stack对象
// 此时无论Myqueue中是否实现了析构函数,都会调用Stack中的析构函数
return 0;
}
6.如果类中并没有申请资源,析构函数就可以不写,因为没有空间需要释放,就可以不写,也可以写,但是一旦涉及到了资源的申请,就需要写析构函数
7.C++规定,后定义的先析构
// 两个Stack实现队列
class MyQueue
{
public:
~MyQueue()
{
cout << "~MyQueue()" << endl;
}
private:
Stack pushst;//7.3再后析构
Stack popst;//7.2再析构
};
int main()
{
//7.C++规定,后定义的先析构
Stack st;//7.4最后析构
MyQueue mq;//7.1先析构
return 0;
}
6.拷贝构造函数
6.1什么是拷贝构造函数
如果一个构造函数的第一个参数是自身类型的引用,且任何额外的参数都有默认值,那么这个构造函数就被称为拷贝构造函数
6.2拷贝构造函数的使用
拷贝构造函数使用注意事项总结:
1.拷贝构造函数是构造函数的一个重载
2.拷贝构造函数的第一个参数必须是类类型的引用,使用传值传参编译器会直接报错,因为这样会造成无限递归问题。拷贝构造函数可以有多个参数,但是第一个参数必须是类类型的引用,后边的参数必须带有缺省值
3.C++规定,自定义类型对象进行拷贝行为必须调用拷贝构造,所以自定义类型传值传参和传值返回都要调用拷贝构造
4.如果没有显示定义拷贝构造,编译器会自动生成一个拷贝构造,自动生成的拷贝构造会对内置类型按照字节进行浅拷贝,也就是一个字节一个字节地拷贝,这样就会形成一个问题:如果是动态开辟的内存的会,对指针的拷贝也会是一个字节一个字节的拷贝,正确做法应该是重新申请一块新的内存空间来使用,所以这里有一个小技巧:如果一个类显式析构并释放资源,那么它就需要显式拷贝构造函数,否则使用编译器形成的拷贝构造即可
5.拷贝构造函数的使用十分方便而且简洁
6.传值返回,会产生一个临时对象调用拷贝构造,传值引用返回,返回的是对象的别名,不会产生拷贝,但是如果返回对象是当前作用域的局部对象,在函数结束之后,这个对象就会销毁,这样返回的引用就是一个野引用,野引用和野指针相似,危害较大,传引用可以减少拷贝,但是一定要确保返回对象在函数结束前后都存在
1.>1.拷贝构造函数是构造函数的一个重载
2.拷贝构造函数的第一个参数必须是类类型的引用
//拷贝构造函数的使用
class Date
{
public:
//构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
//1.拷贝构造函数是构造函数的一个重载
//2.拷贝构造函数的第一个参数必须是类类型的引用,而且额外的参数必须都有默认值
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;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
2.拷贝构造函数的第一个参数必须是类类型的引用,使用传值传参编译器会直接报错,因为这样会造成无限递归问题。拷贝构造函数可以有多个参数,但是第一个参数必须是类类型的引用,后边的参数必须带有缺省值
3.C++规定,自定义类型对象进行拷贝行为必须调用拷贝构造,所以自定义类型传值传参和传值返回都要调用拷贝构造
//拷贝构造函数的使用
class Date
{
public:
//构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//2.使用传值传参编译器会直接报错,因为自定义类型进行拷贝行为必须调用拷贝构造
// 在第一次传参时会调用一次拷贝构造,而拷贝构造又需要调用一次拷贝构造
// 所以这里就会出现无限递归的情况
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;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
4.如果没有显示定义拷贝构造,编译器会自动生成一个拷贝构造,自动生成的拷贝构造会对内置类型按照字节进行浅拷贝,也就是一个字节一个字节地拷贝,这样就会形成一个问题:如果是动态开辟的内存的会,对指针的拷贝也会是一个字节一个字节的拷贝,正确做法应该是重新申请一块新的内存空间来使用,所以这里有一个小技巧:如果一个类显式析构并释放资源,那么它就需要显式拷贝构造函数,否则使用编译器形成的拷贝构造即可
5.拷贝构造函数的使用十分方便而且简洁
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(const Stack& st)
{
//4.我们创建栈时,会通过malloc开辟空间,所以说这里的拷贝构造函数就需要重新开辟空间以供使用
// 其它的值也需要进行拷贝
_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
if (nullptr == _a)
{
perror("malloc申请空间失败!!!");
return;
}
memcpy(_a, st._a, sizeof(STDataType) * st._top);
_top = st._top;
_capacity = st._capacity;
}
void Push(STDataType x)
{
if (_top == _capacity)
{
int newcapacity = _capacity * 2;
STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top++] = x;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
//两个Stack实现队列
class MyQueue
{
public:
private:
Stack pushst;
Stack popst;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
//4.Stack不显⽰实现拷⻉构造,⽤⾃动⽣成的拷⻉构造完成浅拷⻉
//5.拷贝构造的使用:
// 5.1 Stack st2(st1);
// 5.2 Stack st2 = st1;
Stack st2(st1);
Stack st2 = st1;
MyQueue mq1;
//4.MyQueue⾃动⽣成的拷⻉构造,会⾃动调⽤Stack拷⻉构造完成pushst/popst
// 的拷⻉,只要Stack拷⻉构造⾃⼰实现了深拷⻉,他就没问题
MyQueue mq2 = mq1;
return 0;
}
6.传值返回,会产生一个临时对象调用拷贝构造,传值引用返回,返回的是对象的别名,不会产生拷贝,但是如果返回对象是当前作用域的局部对象,在函数结束之后,这个对象就会销毁,这样返回的引用就是一个野引用,野引用和野指针相似,危害较大,传引用可以减少拷贝,但是一定要确保返回对象在函数结束前后都存在
Date& Func2()
{
Date tmp(2024, 7, 5);
tmp.Print();
return tmp;
}
//6.Func2返回了⼀个局部对象tmp的引⽤作为返回值
// Func2函数结束,tmp对象就销毁了,相当于了⼀个野引⽤
Date ret = Func2();
7.运算符重载
7.1什么是运算符重载
对于内置类型,编译器会自动处理它们的+、-、*、/等多种运算,但是对于自定义类型,比如类类型,编译器就不一定能够对它们实现各种运算操作,这时,对于手动实现的运算符操作,就被称为运算符重载
7.2运算符重载的使用
运算符重载是具有特殊名字的函数,既然是函数,就可以通过调用的方式来使用,运算符重载的定义也比较简单,方法为关键字operator和需要重载的运算符即可,大体上和函数定义方法类似
//运算符重载的定义
//假设我们定义了一个日期类
class Date
{
public:
//构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
//此时我们需要重载<,实现日期类型的比较
bool operator<(const Date& x1, const Date& x2)
{
//实现日期类的方法比较简单,只需要比较年、月、日即可
if (x1._year < x2._year)
{
return true;
}
else if (x1._year == x2._year
&& x1._month < x2._month)
{
return true;
}
else if (x1._year == x2._year
&& x1._month == x2._month
&& x1._day < x2._day)
{
return true;
}
return false;
}
int main()
{
//构造函数,初始化对象
Date d1(2024, 9, 2);
Date d2(2024, 10, 1);
//7.2如果我们要实现类类型的比较的话,就需要使用我们实现的运算符重载
// 7.2.1使用方法1:直接当成函数调用来使用
bool ret1 = operator<(d1, d2);
// 7.2.2使用方法2:直接比较
bool ret2 = d1 < d2;
return 0;
}
但是这里就会存在一个问题:因为_year、_month、_day都是私有的,所以函数重载不能够使用,解决这个问题有三个方法:
//为了解决私有成员不能够访问的问题,有三个解决方法:
//1.将私有成员private 变为公有成员 public
//2.Date中提供各种Get()函数,用来提取各种私有成员的值
//3.友元函数
//4.重载为成员函数
bool operator<(const Date& x1, const Date& x2)
{
if (x1._year < x2._year)
{
return true;
}
else if (x1._year == x2._year
&& x1._month < x2._month)
{
return true;
}
else if (x1._year == x2._year
&& x1._month == x2._month
&& x1._day < x2._day)
{
return true;
}
return false;
}
int main()
{
Date d1(2024, 9, 2);
Date d2(2024, 10, 1);
bool ret1 = operator<(d1, d2);
bool ret2 = d1 < d2;
return 0;
}
我们一般使用友元函数或者重载为成员函数的方式来解决
7.3运算符重载使用注意事项
运算符重载使用注意事项总结:
1.如果将重载函数置为内置函数的话,在使用时传入的参数应该减少一个,因为内置类型函数的参数会存在一个this指针,C++规定,二元运算符左侧运算对象传给第一个参数,也就是this指针,第二个运算对象传给第二个参数
2.运算符重载以后,其优先级和与之对应的内置类型运算的优先级保持一致
3.不能够重载连接语法中没有的符号,比如说operator@
4…*、::、sizeof、?:、.、这五个操作符是不能够重载的,我们只需要看一下之前没有见过的.*运算符即可
5.我们不能够通过重载运算符改变内置类型操作符的含义,比如:void operator+(int x, int y),err:必须至少有一个类类型的形参
6.重载++运算符时,有前置++和后置++两种类型,C++规定,重载后置++时,增加一个int形参,方便跟前置++进行区分
7.重载<< 和 >>运算符时,因为重载为成员函数会有一个隐藏的this指针默认接收第一个参数,在使用的过程会很麻烦,所以我们就要将重载函数变为全局函数,将istream\ostream放在第一个形参的位置,将类类型的对象放在第二个形参的位置对应即可
1.如果将重载函数置为内置函数的话,在使用时传入的参数应该减少一个,因为内置类型函数的参数会存在一个this指针,C++规定,二元运算符左侧运算对象传给第一个参数,也就是this指针,第二个运算对象传给第二个参数
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//此时,我们将运算符的重载置为内置成员函数
bool operator<(const Date& d2)
{
if (_year < d2._year)
{
return true;
}
else if (_year == d2._year
&& _month < d2._month)
{
return true;
}
else if (_year == d2._year
&& _month == d2._month
&& _day < d2._day)
{
return true;
}
return false;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 9, 2);
Date d2(2024, 10, 1);
/*bool ret1 = operator<(d1, d2);
bool ret2 = d1 < d2;*/
//在使用时,也会有一些差别
bool ret1 = d1.operator<(d2);
bool ret2 = d1 < d2;
return 0;
}
4…*、::、sizeof、?:、.、这五个操作符是不能够重载的,我们只需要看一下之前没有见过的.*运算符即可
//.*运算符
//.*运算符其实就是访问对象的成员的操作符
//假设我们定义了一个类
class Test
{
public:
//定义一个函数,用来演示.*运算符
void test()
{
cout << "test()" << endl;
}
};
int main()
{
//我们定义一个函数指针,函数指针的名称为pf
void (Test:: * PF) ();
//让pf指针指向Test类中创建的test函数
PF = &Test::test;
//这时我们就可以通过.*运算符来访问test函数
Test t1;
(t1.*PF)();
return 0;
}
5.我们不能够通过重载运算符改变内置类型操作符的含义,比如:void operator+(int x, int y),err:必须至少有一个类类型的形参
void operator+(int x, int y)//err:必须有一个类类型的形参
{
}
8.赋值运算符重载
8.1什么是赋值运算符重载
赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象的直接拷贝赋值,类似于a=b,这里要注意跟拷贝构造函数进行区分,拷贝构造函数是用于拷贝初始化给另一个要创建的对象
8.2运算符重载使用的注意事项
运算符重载使用注意事项总结:
1.赋值运算符重载必须重载为成员函数,这是规定,赋值运算符重载的参数建议加上const当前类型引用,这样即可以确保程序的正确性,又可以避免传值拷贝
2.有返回值,且建议写成类类型的引用,引用返回可以提高效率,避免了拷贝,而且引用返回可以支持连续赋值操作
3.当没有显式实现赋值运算符重载时,编译器会提供一个默认的运算符重载,默认运算符重载会对成员变量进行浅拷贝(一个字节一个字节地拷贝)
4.所以说,对于Date类类型,没有显式申请资源,使用系统自己提供的默认赋值运算符即可完成我们想要的操作,但是对于Stack类型,就需要自己实现赋值运算符重载,这里有一个小技巧:如果一个类显式实现了析构函数并释放资源,那么就需要显式写赋值运算符重载,否则就不需要
1.赋值运算符重载必须重载为成员函数,这是规定,赋值运算符重载的参数建议加上const当前类型引用,这样即可以确保程序的正确性,又可以避免传值拷贝
//赋值运算符重载
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Date(Date& d1)
{
_year = d1._year;
_month = d1._month;
_day = d1._day;
}
//赋值运算符重载
void operator=(const Date& d1)
{
_year = d1._year;
_month = d1._month;
_day = d1._day;
}
//打印
void Print()
{
cout << _year << "年 " << _month << "月 " << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 9, 3);
Date d2(d1);//拷贝构造函数是为创建对象初始化
Date d3;
//赋值运算符重载是为两个已经存在的两个类类型进行赋值操作
d1.Print();
d2.Print();
d3.Print();
d3 = d1;//赋值运算符重载
d1.Print();
d2.Print();
d3.Print();
return 0;
}
2.有返回值,且建议写成类类型的引用,引用返回可以提高效率,避免了拷贝,而且引用返回可以支持连续赋值操作
int main()
{
Date d1(2024, 9, 3);
Date d2(d1);
Date d3;
Date d4;
d1.Print();
d2.Print();
d3.Print();
d3 = d1;//赋值运算符重载
//当我们这样赋值时,会报错,因为d1 = d2赋值操作之后没有返回值,就相当于void = d3,就会报错
d1 = d2 = d3;
d1.Print();
d2.Print();
d3.Print();
return 0;
}
所以我们要进行改进:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d1)
{
_year = d1._year;
_month = d1._month;
_day = d1._day;
}
//赋值运算符重载
//改变点:返回类型变成Date&
Date& operator=(const Date& d1)
{
_year = d1._year;
_month = d1._month;
_day = d1._day;
return *this;
}
void Print()
{
cout << _year << "年 " << _month << "月 " << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 9, 3);
Date d2(d1);
Date d3;
Date d4;
d1.Print();
d2.Print();
d3.Print();
d3 = d1;//赋值运算符重载
d1 = d2 = d3;//当我们将赋值运算符重载的返回值改为Date&时,d1 = d2就会返回成Date类型
//也就是Date = d3,左右运算符类型相同,所以符合规范
d1.Print();
d2.Print();
d3.Print();
return 0;
}
9. 日期类的实现
了解了以上知识之后,我们可以自己实现一个日期类,包含的功能有日期之间的各种操作(加、减等)
9.1比较类型的运算符重载
比较类型的运算符的重载较为简单,只需要对比一下年、月、日即可,但是要注意使用函数的复用,这样能够使问题变得更简单
//1.比较类型的运算符重载
bool Date::operator>(const Date& d1)
{
//分别比较年、月、日即可
if (_year > d1._year)
{
return true;
}
if (_year == d1._year
&& _month > d1._month)
{
return true;
}
if (_year == d1._year
&& _month == d1._month
&& _day > d1._day)
{
return true;
}
return false;
}
bool Date::operator==(const Date& d1)
{
return _year == d1._year && _month == d1._month && _day == d1._day;
}
bool Date::operator<(const Date& d1)
{
//此时可以使用函数的复用
return !(*this > d1 || *this == d1);
}
bool Date::operator>=(const Date& d1)
{
return *this > d1 || *this == d1;
}
bool Date::operator<=(const Date& d1)
{
return !(*this > d1);
}
bool Date::operator!=(const Date& d1)
{
return !(*this == d1);
}
9.2运算类型运算符重载
对于日期的运算是比较有意义的,所以我们可以实现日期的各种运算法,但是最重要的是运算的思路
只要了解了运算符重载的思路之后,编写运算符重载就显得较为简单了:
//2.运算类型的运算符重载
//获取每一个月份天数的函数
int GetMonthDay(int year, int month)
{
//先将每一个月份的天数放在数组中,这里假设是平年的二月
//这里使用static可以将数组放到静态区,防止每次使用函数都需要开辟一块空间
static int arr[13] = {
-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
//如果是闰年的二月,就需要返回29天
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
return 29;
}
return arr[month];
}
Date Date::operator+(int day)
{
//因为加法运算符不会改变原来的值,只有+=会改变,所以我们需要创建一个新的变量
Date tmp = *this;
//使用+=实现+,也是函数复用的思想
tmp += day;
return tmp;
}
Date& Date::operator+=(int day)
{
//如果传入的是一个负数的话,我们还要对负数做出特殊的处理
if (day < 0)
{
*this -= -day;
return *this;
}
//先将天数相加
_day += day;
//判断天数是否超标
while (_day > GetMonthDay(_year, _month) || _day == 0)
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_month = 1;
_year++;
}
}
//有返回值是为了避免d1 + 100 + 100的问题
return *this;
}
Date Date::operator-(int day)
{
//函数复用
Date tmp = *this;
tmp -= day;
return tmp;
}
Date& Date::operator-=(int day)
{
//如果传入的是一个负数的话,我们还要对负数做出特殊的处理
if (day < 0)
{
*this += -day;
return *this;
}
//先让天数直接相减
_day -= day;
//循环处理
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
//前置++
Date& Date::operator++()
{
//前置++是先++后使用
//直接+即可
*this += 1;
return* this;
}
//后置++
Date Date::operator++(int)
{
//后置++为先使用后++
Date tmp = *this;
*this += 1;
return tmp;
}
//前置--
Date& Date::operator--()
{
*this -= 1;
return *this;
}
//后置--
Date Date::operator--(int)
{
Date tmp = *this;
*this -= 1;
return tmp;
}
9.3流插入和流提取运算符重载
流插入和流提取运算符重载起来相对简单,但是需要注意一个相对重要的点
//3.流插入、流提取运算符重载
//检查日期是否合法
bool Date::CheckDate()
{
//只需要检查月和日是否合法即可
if (_month > 12 || _month <= 0
|| _day > GetMonthDay(_year, _month) || _day <= 0)
return false;
else
return true;
}
//流插入运算符
void Date::operator<<(ostream& out)
{
out << _year << "年" << _month << "⽉" << _day << "⽇" << endl;
}
//流提取运算符
void Date::operator>>(istream& in)
{
cout << "请依次输⼊年⽉⽇:>";
while(1)
{
in >> _year >> _month >> _day;
//我们可以检查一下输入的日期是否符合规范
if (!CheckDate())
{
cout << "输入的日期非法,请重新输入" << endl;
}
else
{
break;
}
}
}
这是我们实现的第一份运算符重载,因为C++规定,内置成员函数会包含一个this指针,接收第一个参数,如果我们cout << d1这样写的话,那么cout会被ths指针接受,而d1会被ostream接收,显然是不匹配的,所以使用时只能这样用:d1 << cout,非常不方便,所以我们要将它重载为全局函数,传入两个参数,第一个参数为流参数,第二个为类类型参数:
//先创建一个日期类
class Date
{
//3.流插入、流提取运算符重载
//为了能够访问类中的私有成员,我们需要使用友元函数的概念
friend ostream& operator<<(ostream& out, Date& d1);//流插入运算符
friend istream& operator>>(istream& in, Date& d1);//流提取运算符
public:
//对象实例化
Date(int year = 1, int month = 1, int day = 1);
//拷贝构造函数
Date(Date& d1);
//日期类类型的打印
void Print();
//赋值运算符重载,这里我们需要设计一系列运算符的重载
//1.比较类型的运算符重载
bool operator>(const Date& d1);
bool operator==(const Date& d1);
bool operator<(const Date& d1);
bool operator>=(const Date& d1);
bool operator<=(const Date& d1);
bool operator!=(const Date& d1);
//2.运算类型的运算符重载
Date operator+(int day);
Date& operator+=(int day);
Date operator-(int day);
Date& operator-=(int day);
Date& operator++();//前置++
Date operator++(int);//后置++
Date& operator--();//前置--
Date operator--(int);//后置--
bool CheckDate();//检查日期是否合法
private:
int _year;
int _month;
int _day;
};
//3.流插入、流提取运算符重载
ostream& operator<<(ostream& out, Date& d1);//流插入运算符
istream& operator>>(istream& in, Date& d1);//流提取运算符
----------------------------------------------------------------------------------
//3.流插入、流提取运算符重载
//检查日期是否合法
bool Date::CheckDate()
{
//只需要检查月和日是否合法即可
if (_month > 12 || _month <= 0
|| _day > GetMonthDay(_year, _month) || _day <= 0)
return false;
else
return true;
}
//流插入运算符
ostream& operator<<(ostream& out, Date& d1)
{
out << d1._year << "年" << d1._month << "⽉" << d1._day << "⽇" << endl;
return out;
}
//流提取运算符
istream& operator>>(istream& in, Date& d1)
{
cout << "请依次输⼊年⽉⽇:>";
while (1)
{
in >> d1._year >> d1._month >> d1._day;
//我们可以检查一下输入的日期是否符合规范
if (!d1.CheckDate())
{
cout << "输入的日期非法,请重新输入" << endl;
}
else
{
break;
}
}
return in;
}
10.取地址运算符重载
10.1const成员函数
//const成员函数
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
我们会看到,在Print函数之后,跟了一个const标识符,那么这个标识符到底是什么意思呢?
1.const修饰的成员函数称为const成员函数,用法为将const放在成员函数列表后面
2.const的使用实际上修饰的是隐含的this指针,表明该成员函数不能对类的任何成员做出修改
//const成员函数
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//const修饰的其实是隐含的this指针,所以这个函数原型应该为:
//void Print(const Date* const this)
void Print() const
{
//_year = 2;//err:表达式必须是可以修改的左值
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 10, 10);
d1.Print();
const Date d2(2024, 8, 5);//如果不加const的话:“void Date::Print(void)”: 不能将“this”指针从“const Date”转换为“Date &”
d2.Print();
return 0;
}
10.2取地址运算符重载
取地址运算符重载分为普通的运算符重载和const类型的运算符重载,一般这两个运算符重载编译器自动生成的就已经够我们用了,不需要显式地去写,但是也有极特殊的情况:比如我们不想让别人取到当前类对象的地址,就可以自己实现
//取地址运算符重载
class Date
{
public:
Date* operator&()
{
return this;
//return nullptr; //如果我们不想让别人获取我们类的地址,可以自己实现取地址运算符重载,返回NULL或一个虚假的地址
//return 0x12312ffff
}
const Date* operator&()const
{
return this;
//return nullptr;
}
private:
int _year; //年
int _month; //⽉
int _day; //⽇
};
11.再探构造函数
注意事项总结:
1.之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,但是构造函数的初始化其实还有另外一种赋值方式:初始化列表,初始化列表的使用方式看下面:
2.每个成员变量在初始化列表中只能出现一次, 可以认为初始化列表是每个成员变量定义初始化的地方
3.有三类成员必须使用初始化列表初始化:引用成员变量、const成员变量、没有默认构造函数的类类型变量
4.C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有在初始化列表初始化的成员使用的
5.尽量使用初始化列表初始化,因为你不在初始化列表中初始化的成员也会走初始化列表
6.如果一个成员在初始化列表中定义了初始值,那么就会使用这个初始值,对于没有在初始化列表中初始化的对象,如果在声明位置给了初始值,使用声明时的初始值,如果都没有定义初始值,看下面的图:
7.初始化列表中的成员变量在类中按声明顺序进行初始化,与成员在初始化列表中出现的先后顺序无关,所以建议声明顺序和初始化列表顺序保持一致
1.之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,但是构造函数的初始化其实还有另外一种赋值方式:初始化列表,初始化列表的使用方式看下面:
2.每个成员变量在初始化列表中只能出现一次, 可以认为初始化列表是每个成员变量定义初始化的地方
//初始化列表
class Data
{
public:
//1.初始化列表的使用
//2.每个成员变量在初始化列表中只能出现一次
Data(int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{
}
private:
int _year;
int _month;
int _day;
};
int main()
{
return 0;
}
3.有三类成员必须使用初始化列表初始化:引用成员变量、const成员变量、没有默认构造函数的类类型变量
//
class Test
{
public:
Test(int& r)
:_ca(r) //1.引用必须使用初始化列表进行初始化
,_cb(1) //2.const成员变量初始化必须使用初始化列表
{
//_ca = r;//err: 必须初始化引用
//_cb = 20;//err
}
private:
int& _ca;
const int _cb;
};
//
//假设我们上面已经实现了栈,而且实现的栈中没有构造函数
class MyQueen
{
public:
//如果我们实现的栈中没有构造函数,就需要使用初始化列表对队列进行初始化
MyQueen(int n = 1000) //3.没有构造函数的类类型变量必须使用初始化列表进行初始化
:_pushst(n)
,_popst(n)
{
}
private:
Stack _pushst;
Stack _popst;
};
int main()
{
int n = 20;
Test t1(n);
return 0;
}
4.C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有在初始化列表初始化的成员使用的
class Data
{
public:
//如果在初始化列表中没有对变量进行初始化的话,就会使用声明时给的缺省值
Data(int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
{
}
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
//4.在成员变量声明的位置给缺省值
int _year = 2;
int _month = 2;
int _day = 2;
};
int main()
{
Data d1;
d1.Print();//1 - 1 - 2
return 0;
}
5.尽量使用初始化列表初始化,因为你不在初始化列表中初始化的成员也会走初始化列表
class Time
{
public:
Time(int hour)
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
{
}
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
//5.对于_day和_t,都没在初始化列表中初始化,但是它们都会走初始化列表
int _day = 3;
Time _t = 10;//其实就相当于在初始化列表中写:_t(10)
};
int main()
{
Date d1;
d1.Print();
return 0;
}
6.如果一个成员在初始化列表中定义了初始值,那么就会使用这个初始值,对于没有在初始化列表中初始化的对象,如果在声明位置给了初始值,使用声明时的初始值,如果都没有定义初始值,看下面的图:
7.初始化列表中的成员变量在类中按声明顺序进行初始化,与成员在初始化列表中出现的先后顺序无关,所以建议声明顺序和初始化列表顺序保持一致
下⾯程序的运⾏结果是什么()
A.输出 1 1
B.输出 2 2
C.编译报错
D.输出 1 随机值 √
E.输出 1 2
F.输出 2 1
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{
}
void Print()
{
//_a2先初始化,所以是随机值,_a1后初始化为1
cout << _a1 << " " << _a2 << endl;
}
private:
//按照声明顺序进行初始化,所以先初始化_a2,再初始化_a1
int _a2 = 2;
int _a1 = 2;
};
int main()
{
A aa(1);
aa.Print();
return 0;
}
12.类型转换
class A
{
public:
A(int a1)
:_a1(a1)
{
}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a1 = 1;
int _a2 = 2;
};
int main()
{
A aa1 = 1;
aa1.Print();
return 0;
}
通过看上面的一串代码我们能够发现,1竟然从int类型转换成了A类类型,这其实就是隐式类型转换,转换原理为:1构造一个临时的A类型对象,然后再用这个对象拷贝构造给我们的aa1,这里编译器会做出优化,会优化为直接构造
class A
{
public:
A(int a1)
:_a1(a1)
{
}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a1 = 1;
int _a2 = 2;
};
int main()
{
//1构造一个A的临时对象,再用这个临时对象拷贝构造aa1
//此时编译器会进行优化:优化为直接构造
A aa1 = 1;
aa1.Print();
//这个也是成立的,注意1具有常性,需要加上const修饰
const A& aa2 = 1;
return 0;
}
在构造函数前面加上explicit关键字,就不再支持隐式类型转换
class A
{
public:
explicit A(int a1)
:_a1(a1)
{
}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a1 = 1;
int _a2 = 2;
};
int main()
{
A aa1 = 1;//加上了explicit关键字之后,err:无法从int转换为A
aa1.Print();
const A& aa2 = 1;
return 0;
}
类类型的对象之间也支持隐式类型转换,但是需要相应的构造函数支持
class A
{
public:
A(int a1)
:_a1(a1)
{
}
A(int a1, int a2)
:_a1(a1)
, _a2(a2)
{
}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
int Get() const
{
return _a1 + _a2;
}
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
B(const A& a)
:_b(a.Get())
{
}
private:
int _b;
};
int main()
{
A aa1 = 1;
//在C++11之后,支持了多参数转化的功能,用法如下:
A aa2 = {
2, 2 };
//aa1隐式转换成B对象,原理和之前相似
B bb1 = aa1;
const B& rb = aa2;
return 0;
}
13.static成员
static成员使用注意总结:
1.用static修饰的成员变量,称为静态成员变量,静态成员变量规定在类外初始化
2.静态成员变量不属于任何对象的实例,在类的作用域之内,但是在类的对象之外,存放在静态区,初始化列表是为类成员变量进行初始化的,所以不能在声明、以及初始化列表对静态成员进行初始化
3.用static修饰的成员函数,称为静态成员函数,静态成员函数没有this指针
4.静态成员函数可以访问其它的静态成员,但是不能访问非静态的,因为没有this指针
5.而非静态的成员函数,可以访问任意的静态成员变量和静态成员函数
6.突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员来访问静态成员变量和静态成员函数
7.静态成员也是类的成员,受public、protected、private访问限定符的限制
1.用static修饰的成员变量,称为静态成员变量,静态成员变量规定在类外初始化
2.静态成员变量不属于任何对象的实例,在类的作用域之内,但是在类的对象之外,存放在静态区,初始化列表是为类成员变量进行初始化的,所以不能在声明、以及初始化列表对静态成员进行初始化
class A
{
public:
A()
//:_count(1)//err: count不是类A的非静态数据成员或基类
{
++_count;
}
private:
//static int _count = 1;//err
static int _count;
};
//static修饰的成员变量必须在类外初始化
int A::_count = 0;
3.用static修饰的成员函数,称为静态成员函数,静态成员函数没有this指针
4.静态成员函数可以访问其它的静态成员,但是不能访问非静态的,因为没有this指针
5.而非静态的成员函数,可以访问任意的静态成员变量和静态成员函数
6.突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员来访问静态成员变量和静态成员函数
7.静态成员也是类的成员,受public、protected、private访问限定符的限制
当没有静态成员函数时:
class A
{
public:
A()
{
++_count;
}
int GetACount()
{
return _count;
}
private:
static int _count;
};
int A::_count = 0;
void Fxx()
{
//当没有静态成员函数时,我们如果想要访问A中的GetACount函数,就需要再实例化一个A对象
//然后通过aa2对象使用该函数,这样是很别扭的
A aa2;
cout << aa2.GetACount() << endl;
}
int main()
{
A aa1;
cout << aa1.GetACount() << endl;
Fxx();
return 0;
}
当使用静态函数时:
class A
{
public:
A()
{
++_count;
}
static int GetACount()
{
return _count;
}
private:
static int _count;
};
int A::_count = 0;
void Fxx()
{
//当存在静态成员函数时,就可以直接在函数内访问静态成员函数或静态成员变量
//但是仍然是类的成员,受访问限定符的影响
cout << A::GetACount() << endl;
cout << A::_count << endl; //err:无法访问private成员
}
int main()
{
A aa1;
cout << aa1.GetACount() << endl;
Fxx();
return 0;
}
13.1static成员题目1
链接: 求1+2+3+…+n
class Sum
{
public:
Sum()
{
_count += _i;
_i++;
}
static int GetRet()
{
return _count;
}
private:
static int _count;
static int _i;
};
int Sum::_count = 0;
int Sum::_i = 1;
class Solution
{
public:
int Sum_Solution(int n)
{
// 变⻓数组
Sum arr[n];
return Sum::GetRet();
}
};
13.2题目2
设已经有A, B, C, D 4个类的定义,程序中A, B, C, D构造函数调⽤顺序为?()//C A B D
设已经有A, B, C, D 4个类的定义,程序中A, B, C, D析构函数调⽤顺序为?()//B A D C
A:D B A C
B:B A D C
C:C D B A
D:A B D C
E:C A B D
F:C D A B
//对于构造函数的顺序:
//全局变量肯定会在main函数之前就构造,而对于静态成员,它的构造是在第一次第一次调用的时候才会初始化
//对于析构函数的顺序:
//首先是局部对象的析构,对于全局和局部静态的析构,一定是局部静态的先析构,后是全局的析构
C c;
int main()
{
A a;
B b;
static D d;
return 0;
}
14.友元
在之前我们就展示过友元函数的概念,而且友元较为简单,容易理解,所以这里不会过多解释:
1.友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类声明的前面加上friend,并且把友元声明放在一个类里面
2.外部友元函数可以访问类的私有成员和保护成员,友元函数只是一种声明,并不是类的成员函数
3.友元函数可以在类定义的任何地方声明,不受访问限定符的限制,也就是说,它甚至可以放在private内部
4.一个函数可以是多个类的友元函数
5.友元类的成员函数都可以是另一个类的友元函数,都可以访问另一个类中私有和保护成员
6.友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元
7.友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元
8.友元函数不宜多用,低耦合、高内聚,友元函数会提高耦合度,如果一个类出现错误,可能会导致很多类都出现错误,所以友元不宜多用
友元函数示例:
// 前置声明,都则A的友元函数声明编译器不认识B
class B;
class A
{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
// 友元声明
friend void func(const A& aa, const B& bb);
private:
int _b1 = 3;
int _b2 = 4;
};
void func(const A& aa, const B& bb)
{
cout << aa._a1 << endl;
cout << bb._b1 << endl;
}
int main()
{
A aa;
B bb;
func(aa, bb);
return 0;
}
类友元示例:
class A
{
// 友元声明
friend class B;
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
void func1(const A& aa)
{
cout << aa._a1 << endl;
cout << _b1 << endl;
}
void func2(const A& aa)
{
cout << aa._a2 << endl;
cout << _b2 << endl;
}
private:
int _b1 = 3;
int _b2 = 4;
};
int main()
{
A aa;
B bb;
bb.func1(aa);
bb.func1(aa);
return 0;
}
15.内部类
内部类这一个知识点的使用也比较简单,我们总结着来看一下:
1.如果一个类定义在另一个类的内部, 这个内部定义的类就叫做内部类,内部类是一个独立的类,跟定义在全局相比,只是受类域和访问限定符的限制,所以外部类定义的对象中不包含内部类
2.内部类默认是外部类的友元
3.内部类的本质也是一种封装,当A类跟B类紧密关联,A类实现出来的目的就是给B类使用,那么就可以考虑把A类设计为B的内部类,如果放到private或protected位置,那么A类就是B类的专属类,其它地方都用不了
内部类的使用:
class A
{
private:
static int _k;
int _h = 1;
public:
class B // B默认就是A的友元
{
public:
void foo(const A& a)
{
cout << _k << endl;
cout << a._h << endl;
}
};
};
int A::_k = 1;
int main()
{
//输出的结果为4,静态成员不属于A类的对象,所以A类的大小只有一个_h,也就是4个字节
//所以这个就可以证明:A类不包含B类,B类只是受到A类的类域和访问限定符的限制
cout << sizeof(A) << endl;
A::B b;
A aa;
b.foo(aa);
return 0;
}
之前的1+2+3+…+n题目使用新知识改写:
#include <climits>
class Solution {
private:
static int _count;
static int _i;
class Sum{
public:
Sum()
{
_count += _i;
_i++;
}
};
public:
int Sum_Solution(int n) {
Sum arr[n];
return _count;
}
};
int Solution::_count = 0;
int Solution::_i = 1;
16.匿名对象
1.用类型(实参)定义出来的对象叫做匿名对象,我们之前使用类型+对象名(实参)定义的对象叫做有名对象
2.匿名对象的声明周期只有当前这一行,一般临时定义一个对象当前用一下即可,就可以使用匿名对象
//匿名对象
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);
//如果我们只是单纯想要调用Sum_Solution函数,我们可以这么写:
Solution s1;
s1.Sum_Solution(10);
//Sum_Solution(10)//err:Sum_Solution找不到标识符
//匿名对象在这样场景下就很好⽤,当然还有⼀些其他使⽤场景,这个我们以后遇到了再说
Solution().Sum_Solution(10);
return 0;
}
17.对象拷贝时的编译器优化
现代的编译器为了尽可能地提高程序的效率,在不影响正确性的情况下会尽可能地减少一些传参和返回值的过程中可以省略的拷贝,但是对于如何优化,C++没有严格的规定,具体要看编译器
class A
{
public:
A(int a = 0)
:_a1(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a1(aa._a1)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a1 = aa._a1;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a1 = 1;
};
void f1(A aa)
{
}
A f2()
{
A aa;
return aa;
}
上面展示的是我们的前提代码,后边的思路都是与这串代码紧密联系的,所以我们要了解上面的代码
1.优化情况1
A aa1 = 1;
//在不优化的情况下,需要做出的处理为:使用1构造一个A类的临时对象,再用这个临时对象拷贝构造给aa1
//为此,编译器会做出优化:
//优化为直接构造
优化情况2:隐式类型,连续构造+拷⻉构造->优化为直接构造
f1(1);
//这里一般情况下也应该是构造+拷贝构造,但是编译器优化为直接构造
下面是运行结果,运行结果我们将上边和下边的放在一起了:
优化情况3:⼀个表达式中,连续构造+拷⻉构造->优化为⼀个构造
f1(A(2));
cout << endl;
//本来应该是构造一个匿名对象+拷贝构造,编译器优化为一个构造
优化情况4:
返回时⼀个表达式中,连续拷⻉构造+拷⻉构造->优化⼀个拷⻉构造 (vs2019 debug)
⼀些编译器会优化得更厉害,进⾏跨⾏合并优化,直接变为构造。(vs2022 debug)
A aa2 = f2();
cout << endl;
优化情况5:⼀个表达式中,连续拷⻉构造+赋值重载->⽆法优化
aa1 = f2();
cout << endl;