C++入门 谁都能看懂的类和对象
类
C语言结构体中只能定义变量.
在C++中,结构体内不仅可以定义变量,也可以定义函数。
//c语言
typedef struct ListNode
{int val;struct ListNode* next;
}LTN;
//c++
struct ListNode
{int val;//c++中可以直接用这个,不用加structListNode* next;
};
比如: 之前在数据结构初阶中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现。
//C++用类模拟实现栈
struct Stack
{//可以定义函数void Init(int n = 4){a = (int*)malloc(sizeof(int) * n);if (nullptr == a){perror("malloc fail");exit(-1);}capacity = n;size = 0;}void Push(int x){//...a[size++] = x;}//成员变量int* a;int size;int capacity;
};int main()
{Stack st;//就像访问结构体成员一样,访问函数st.Init();st.Push(1);st.Push(2);st.Push(3);st.Push(4);return 0;
}
一、类的定义
在C++中虽然,
struct
可以作为定义类的 关键字,但是
一般使用class
作为定义类的关键字,{}
中的内容成为 类的主体
类体中内容称为类的成员
类中的变量称为类的属性或成员变量;
类中的函数称为类的方法或者 成员函数。
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
类的两种定义方式
1.声明和定义全部放在类体中
- 注意
- 声明和定义全部放在类体中,
需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
其实就是像我们上面这种。
struct Stack
{//可以定义函数void Init(int n = 4){a = (int*)malloc(sizeof(int) * n);if (nullptr == a){perror("malloc fail");exit(-1);}capacity = n;size = 0;}void Push(int x){//...a[size++] = x;}//成员变量int* a;int size;int capacity;
};int main()
{Stack st;//就像访问结构体成员一样,访问函数st.Init();st.Push(1);st.Push(2);st.Push(3);st.Push(4);return 0;
}
2.类声明放在.h文件中,成员函数定义放在.cpp文件中
这个其实就是声明和定义分离,我们需要注意的一点就是
成员函数名前需要加类名::
,因为我们需要找到这个域,从这个域中找到这个函数
stack.h
的内容
struct Stack
{//可以定义函数void Init(int n = 4);void Push(int x);int* a;int size;int capacity;
};
stack.c
的内容
//缺省参数一般在声明处定义
void Stack::Init(int n )
{a = (int*)malloc(sizeof(int) * n);if (nullptr == a){perror("malloc fail");exit(-1);}capacity = n;size = 0;
}
void Stack::Push(int x)
{//...a[size++] = x;
}
二、访问限定符
有的同学尝试用
class
代替struct
,发现遍不过去,其实就是我们的访问限定符的作用
访问限定符的种类
【访问限定符说明】
- public修饰的成员在类外可以直接被访问
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到 } 即类结束。
class
的默认访问权限为private
,struct
为public
(因为struct要兼容C)
举例说明,如果在声明和定义分离,一般在声明中设置访问限定符
class Stack
{//一般我们的成员函数设定为公有
public:void Init(int n = 4);void Push(int x);//我们的成员变量设定为私有
private:int* _a;int _size;int _capacity;
};
在成员变量的命名习惯:在成员变量前加一个
_
,防止当我的成员函数用相同的名字的变量起冲突。
三、封装
将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理,让用户更方便使用类。
四、类的实例化
在理解类的实例化,我们需要先理解另一个知识点。
在我们定义类的时候,我们知道他的内容是成员函数和成员变量。
这时我提一个问题,我们的成员变量他是声明还是定义?到底开没有空间。
答案:其实我们定义的成员变量是声明。并且他没有开辟空间。
其实我们可以这样理解,我们的类,就相当于一个建房子的一个图纸,只是定义了我们怎么去建造房子,并没有建好房子,而建房子这个就叫做实例化。
类的实例化其实是这样的
class Data
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}
private://这个只是声明,并没有开辟空间int _year;int _month;int _day;};int main()
{//这个其实就是我们类的实例化//开辟空间Data d1;Data d2;
}
五、类对象的大小
- 什们是类的对象?
- 在我们类的实例化的时候,我们定义类的内容就是我们的对象。
看上面代码就是我们的d1
和d2
.
类中包括成员函数和成员变量,
但是我们的规定我们类的对象只包括类的成员变量,不包括成员函数。
- 怎么计算类对象的大小
- 所以我们计算类对象的大小就只计算我们的成员变量,并且计算成员变量的时候我们要像结构体一样,考虑内存对齐。 为什们类的对象不包括成员函数?
- 我们知道每个对象的成员变量是不一样的,需要独立存储。
但是我们每个对象成员函数都是一样的,我们放到公共区域(这个区域叫做代码段)
看下图自己算几个,
六、this
指针
我们会不会产生一个问题,当我们在使用类的函数的时候,每个类的对象都有可能会用这个函数,并且传的参数类型和数量都一样,那么编译器是怎么分清楚那个是那个数据是那个对象的。
其实编译器会自己做一件事情,这件事情我们做不了,只有编译器可以做,具体怎么做看下图。
- 存放在栈中,因为它是局部变量,并且他还是隐含形参。
但是在vs下面是通过ecx寄存器
this
指针存在哪里?练习题
class Data
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void func(){cout << this << endl;cout << func << endl;}//这个只是声明,并没有开辟空间int _year;int _month;int _day;};int main()
{Data* ptr = nullptr;//下面代码会不会报错?ptr->func(); //正常运行ptr->Init(2022, 3, 2); //报错(*ptr).func(); //正常运行ptr->_year;//运行崩溃
}
对于第一个
ptr->func()
为什们正常运行,好多人以为是崩溃。
首先我们知道ptr
是我们对象的指针,首先我们要明白一点,不是见了->
就是解引用。
对于对象,他包括的是成员变量,但是我们的成员函数并不属于我们的对象,它存在于公共的区域(代码段),而ptr
的作用,就是带我们找到这个域,并将这个这个ptr
传给成员函数的this
,也就是空指针,在我们这个成员函数没有发生空指针的解引用,只是将这个空指针this
打印出来。
对于第二个,我们再找到对于的成员函数并不会崩溃,但是在这个函数中发生了空指针解引用,所以才会崩溃。
有的同学会问,哪里有空指针的解引用,大家看这个代码_year = year;
,我们通过上面的学习我们怎么知道_year
是那个对象的,是用this
指针找到的。所以就在这里发生了空指针的解引用。
对于第三个其实原理跟第一个原理一样,他的作用就是带我们找到这个域,并将
this
传个成员函数。
对于第四个:他直接崩溃,因为
_year
是成员变量,存在于对象中,而直接就是空指针的解引用.
类的6个默认成员函数
- 默认成员函数的特点
- 我们如果不写,编译器会自动生成一个默认的,但是如果我们实现了,编译器就不自动生成了。
一、构造函数
- 为什们要有构造函数?
- 其实这个问题很简单,假如我们要写一个栈,我们每次在使用前我们都要初始化,但是如果我们忘了怎么办呢?突然有一次没有写这个初始化,那么我们程序就会崩溃,
所以既然我们每次使用都要调用初始化,为什们不交给编译器来做呢?所以构造函数应运而生。
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
Ⅰ、构造函数的特征
- 函数名与类名相同。
- 无返回值。(注意并不是
void
,而就是不写返回值) - 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。(一个类可以有多个构造函数,有多种初始化方式)
Ⅱ、写一个构造函数
struct Stack
{
public://构造函数Stack(){_a = nullptr;_size = _capacity = 0;}//构造函数的函数重载Stack(int n){_a = (int*)malloc(sizeof(int) * n);if (nullptr == _a){perror("malloc fail");exit(-1);}_capacity = n;_size = 0;}void Push(int x){//..._a[_size++] = x;}
private:int* _a;int _size;int _capacity;
};
int main()
{//构造函数无参Stack st;//Stack st(); 无参不能这样传参,就不用加括号//构造函数有参数Stack st1(4);//就像访问结构体成员一样,访问函数st.Push(1);st.Push(2);st.Push(3);st.Push(4);return 0;
}
Ⅲ、默认构造函数
我们在上面六个默认成员的特点上面写了,
如果自己没有写构造函数,系统会自动生成一个默认构造函数。
- C++ 的类型
-
- 内置类型。编译器本身带的(int/char/double/指针)
-
- 自定义类型。class/struct定义的
编译器生成的默认构造函数 -
- 内置类型成员不做处理。
-
- 自定义类型的成员,会去调用自定义类型的默认构造(不用传参数的构造)
①内置类型的默认构造函数
看下面的例子
struct Data
{
public:void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}
private:int _year;int _month;int _day;
};int main()
{Data d1;d1.Print();return 0;
}
我们发现打印的都是随机值,其实他有默认构造函数,但是因为是内置类型,所以并没有做处理。
所以当我们用这种日期的类的时候,我们不要用这种默认构造函数。
其实这是一个bug,为什们内置类型没有做处理,这是有问题的。这是设计者的失误。所以设计者打了一个补丁。
内置类型的默认构造函数补丁
既然我们不能改了。
那么我们在声明成员变量的时候用缺省参数,就可以很好的解决这个问题。
注意:这个并不是初始化成员变量。千万不要搞混。
看下面的例子。
struct Data
{
public:void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}
private://声明位置写初始化int _year=1;int _month=1;int _day=1;
};int main()
{Data d1;d1.Print();return 0;
}
②自定义类型的默认构造函数
struct Stack
{
public://构造函数Stack(){_a = nullptr;_size = _capacity = 0;}//构造函数的函数重载Stack(int n){_a = (int*)malloc(sizeof(int) * n);if (nullptr == _a){perror("malloc fail");exit(-1);}_capacity = n;_size = 0;}void Push(int x){//..._a[_size++] = x;}//析构函数~Stack(){free(_a);_a = nullptr;_size = _capacity = 0;}
private:int* _a;int _size;int _capacity;
};class MyQueue {
public:// 默认生成构造函数,对自定义类型成员,会调用他的默认构造函数// 默认生成析构函数,对自定义类型成员,会调用他的析构函数void push(int x) {}//....Stack _pushST;Stack _popST;int _size = 0;
};int main()
{MyQueue q;return 0;
}
Ⅳ、默认构造函数注意
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数
不传参数就可以调用构造函数
一般建议,每个类都提供要给默认构造函数。
二、析构函数
- 为什们要有析构函数
- 跟构造函数一样,析构函数解决初始化的问题,但是假设我们写一个栈,不仅对于他来说,要初始化,更要销毁栈,清理资源。
但是如果我们忘了这件事,我们的程序岂不是要出大问题。
这时我们的析构函数就排上用场了
Ⅰ、析构函数的特性
- 析构函数
- 与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。
而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数是特殊的成员函数,其特征如下:
- 析构函数名是在类名前加上字符
~
。(~就是取反的意思,意思就是构造函数取反) - 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
Ⅱ、默认析构函数
跟构造函数一样,就不多介绍了
默认生成构造函数,对自定义类型成员,会调用他的默认构造函数
默认生成析构函数,
对自定义类型成员,会调用他的析构函数
内置类型成员不做处理。
Ⅲ、写一个析构函数
struct Stack
{
public://构造函数Stack(){_a = nullptr;_size = _capacity = 0;}//构造函数的函数重载Stack(int n){_a = (int*)malloc(sizeof(int) * n);if (nullptr == _a){perror("malloc fail");exit(-1);}_capacity = n;_size = 0;}void Push(int x){//..._a[_size++] = x;}//析构函数~Stack(){free(_a);_a = nullptr;_size = _capacity = 0;}
private:int* _a;int _size;int _capacity;
};
int main()
{//构造函数无参//Stack st;//Stack st(); 无参不能这样传参,就不用加括号//构造函数有参数Stack st(4);//就像访问结构体成员一样,访问函数st.Push(1);st.Push(2);st.Push(3);st.Push(4);//看到return,就自动调用析构函数return 0;
}
三、拷贝构造
Ⅰ、拷贝构造的特征
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
①为什们拷贝构造的参数必须是类对象的引用
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// 直接传内置类型,这是不被允许的,被禁止的//下面解释为什们不能这么做的原因Date(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);//拷贝构造的另一种写法Data d3 = d1;return 0;
}
其实也是很容易理解的,对于编译器的内置类型,编译器会傻瓜式的进行浅拷贝,就是按照字节拷贝。
但是如果对于自定义类型,如果我们进行浅拷贝,可能会发生一些问题。
举个例子:
假如我们的自定义类型栈1
中有一个指针,现在我们需要拷贝到另一个栈2
,但是因为浅拷贝,只将栈1
指针的4个字节传了过来,并没有开辟空间,所以两个栈的指针指向同意块空间。这是非常大的问题。
所以我们不能直接用自定义类型当参数。
//正确的拷贝构造应该用引用
Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}
但是如果我们用引用传参的时候
Date d2(d1);
,
d是d1的别名,我们直接将d1的值赋给d2.这也是比较容易理解
②为什们不用类对象的引用会发生无穷递归
我们看上面的代码,要进行类的实例化
Date d2(d1);
,我们就要调用对应的构造函数Date(Date d)
,但是我们发现构造函数的参数还是一个拷贝构造,然后他的参数就一直是这个就会造成无穷递归。
我们的拷贝构造无限递归是因为,在调用拷贝构造的时候,参数如果不是引用,传参形成一个新的拷贝构造,而调用新的拷贝构造,又要传参,就形成了一个死循环。
Ⅱ、默认拷贝构造函数
若未显式定义,编译器会生成默认的拷贝构造函数。
默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
我们在上面已经介绍了什们叫做浅拷贝,就是按照字节进行拷贝,
对于内置类型,这种默认生成的拷贝构造,是没有问题的。
但是对于自定义类型,如果按照拷贝构造,按照字节进行拷贝,如果有指针,就会将这两个指向同一块空间,会造成很大的问题。
- 为什们自定义类型不能用默认拷贝构造指向同一块空间?
-
- 插入删除数据会互相影响
-
- 析构两次,程序崩溃
默认生成的拷贝构造和赋值重载 - a、内置类型完成浅拷贝/值拷贝
b、自定义类型,去调用这个成员的拷贝构造/赋值重载
什们时候情况下需要实现拷贝构造?
自己实现了析构函数释放空间,就需要实现拷贝构造。
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 10){cout << "Stack(size_t capacity = 10)" << endl;_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申请空间失败");exit(-1);}_size = 0;_capacity = capacity;}void Push(const DataType& data){// CheckCapacity();_array[_size] = data;_size++;}Stack(const Stack& st){cout << "Stack(const Stack& st)" << endl;_array = (DataType*)malloc(sizeof(DataType)*st._capacity);if (nullptr == _array){perror("malloc申请空间失败");exit(-1);}memcpy(_array, st._array, sizeof(DataType)*st._size);_size = st._size;_capacity = st._capacity;}~Stack(){cout << "~Stack()" << endl;if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}private:DataType *_array;size_t _size;size_t _capacity;
};class MyQueue
{
public:// 默认生成构造// 默认生成析构// 默认生成拷贝构造private:Stack _pushST;Stack _popST;int _size = 0;
};int main()
{MyQueue q1;MyQueue q2(q1);return 0;
}
四、运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字
operator
+后面接需要重载的运算符符号。
函数原型:返回值类型operator
操作符(参数列表)
例子:现在我们有两个日期类,我们想比较一下判断一下日期是否相等
我们之前就是写一个函数,然后将两个参数传上去,然后我们在函数内比较。但是呢我们如果是两个内置类型,我们直接就可以用运算符比较编译器就可以比较。
但是我们的自定义类型却不可以,里面的东西比较复杂,所以我们得写一个函数自己去才可以解决。
- 运算符重载的意义
- 自定义类型的对象可以直接用运算符比较。
Ⅰ、将运算符重载放到全局
日期类,比较日期是否相等
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;
}int main()
{Date d1(2023, 2, 11);Date d2(2023, 3, 11);operator==(d1, d2);d1 == d2;//转换成上面这个return 0;
}
我们注意:现在这个程序是有问题的,因为我们类的成员变量是私有的,我们不能访问,现在最简单的操作就是将私有设置为共有,这样我们就可以判断了。
这个函数原型,其实本质就是一个函数,只不过我们的函数名设置成了我们规定的这种。
所以我们在使用的时候,其实可以跟我们使用函数一样,也可以直接用对象进行比较。
operator==(d1, d2);
或d1 == d2;
Ⅱ、将运算符重载放到类中
当我们放到全局中,我们的类的成员变量被我们设置成共有的,收到访问限定符的限制,为了解决这个问题。
我们将这个放到类当中就可以解决这个问题。
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}bool operator==(const Date& d){return _year == d._year &&_month == d._month &&_day == d._day;}
private:int _year;int _month;int _day;
};int main()
{Date d1(2023, 2, 11);Date d2(2023, 3, 11);d1.operator==(d2);d1 == d2;return 0;
}
我们发现跟我们想的不一样,我们在类中的操作数这么就变成一个了,其实是有一个隐藏的操作数传上去了,就是this,而对于这个隐藏操作数就是d1,所以我们只需要传d2就行了。
然后在函数传参的时候,我们也有两种方式,d1.operator==(d2);
和d1 == d2;
他的大于小于就不多写了,其实跟他一样。
就看下面的代码
bool operator==(const Date& d){return _year == d._year &&_month == d._month &&_day == d._day;}bool operator < (const Date& d){if (_year < d._year){return true;}else if (_year == d._year && _month < d._month){return true;}else 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){return !(*this <= d);}bool operator >= (const Date& d){return !(*this < d );}bool operator != (const Date& d){return !(*this == d);}
Ⅲ、 赋值运算符
(1)赋值运算符重载格式
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义
(2)赋值运算符只能重载成类的成员函数不能重载成全局函数.
(3) 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
注 意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符 重载完成赋值。
//d1 = d2//带返回值,是为了连续赋值。Date& operator=(const Date& d){//防止自己给自己赋值if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;}
首先我们解释一点,我们传参可以不用,传引用,他不会像我们拷贝构造函数无穷递归,
因为我们在调用这个的时候函数,进行拷贝构造,然后就直接进行赋值了。但是我们的拷贝构造是因为,在调用拷贝构造的时候,参数如果不是引用,传参形成一个新的拷贝构造,而调用新的拷贝构造,又要传参,就形成了一个死循环。
Ⅳ、运算符重载注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+不能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的
this
。 .*
::
sizeof
?:
.
注意以上5个运算符不能重载。这个经常在笔试选择题中出
Ⅴ、用运算符重载写一个输入流和输出流
①流插入
我们C++中我们输入流和输出流就是用
cin
和cout
。
他们最大的特点就是能够自动识别类型。
我们平常用的
cin>>d
和cout<<d
,这个其实就是运算符重载,<<和>>
就是运算符。
我们要想看明白下面的内容,理解这个是必要的。
cin
和cout
其实就是类的对象。
同时大家需要知道ostream
是cout
对象的类。
istream
是cin
对象的类
- 为什们能够支持自动识别类型?
支持自动识别,是因为库里面已经帮我们写了,并且支持函数重载。当我们写不同类型的时候我们就会调用不同的函数。
所以我们写的cout << d
其实就是cout.operator <<(int)
,通过你传的参,去识别类型。
如果我们想要输入输出一个自定义类型,但是对于自定义类型来说,编译器并不能自动识别,所以需要我们自己用运算符重载一个。
//输出流,放到我们上面的日期类中。
void Date::operator<<(ostream& out)
{out << _year << "年" << _month << "月" << _day << "日"<< endl;
}
但是我们使用的时候,对于我们写的这种类型,不能像
cout << d
这样使用,这样会报错。(d是我们日期类的对象)
利用我们现有的知识,对于运算符重载规定
第一个参数是左操作数,第二个参数是右操作数,
如果是在类中,那么传的时候会将第一个参数省略,传的是this
指针,
对于我们这个函数,this
指的是d
,第二个参数才是cout
。
但是如果cout << d
这样写,传第一个参数就是cout
,第二个参数是d
。
所以我们应该这样写。
d.operator>>(cout);
,也可以这样d1 << cout;
但是这样写跟我们想的完全不一样。
所以我们这时可以不用将我们的运算符重载放到我们的类中。
可以将我们的运算符重载放到全局。
按照需要的参数顺序传参,就没有this
指针了。
具体传参为operator<<(cout, d1);
或cout << d1;
但是可能面对的问题就是不能访问私有的成员变量。这个其实有办法解决。
//加返回值是为了实现连续输出
//为了实现这种场景 cout << d1 <<d2 << endl;
//定义
ostream& operator<<(ostream& out,const Date& d)
{out << _year << "年" << _month << "月" << _day << "日" << endl;return out;
}
- 访问私有成员变量的方法
- 友元函数
具体做法就是将我们这个函数的声明放到我们的类中,在前面加上friend
。
下面会详细介绍,如果不懂可直接跳到下面的友元函数。
//声明
class Date
{//友元函数的声明friend ostream& operator<<(ostream& out, const Date& d);
public://成员函数
private:int _year;int _month;int _day;
};
//在类外面声明ostream& operator<<(ostream& out, const Date& d);
注意有两个声明。
我们将这个运算符重载放到了全局中,我们就再全局中声明,也就是类的外面。
但是为了能够访问类中的成员函数,在类中定义友元函数。
②流提取
跟上面一样,将运算符重载,放到全局中。
并且有返回值,是为了能够多重打印。
cout << d1 <<d2 << endl;
流提取在类中,友元函数的声明
friend istream& operator>>(istream& in,Date& d);
流提取的运算符重载
istream& operator>>(istream& in, Date& d)
{in >> d._year >> d._month >> d._day;return in;
}
测试
Date d;cin >> d;cout << d;
③流提取和流插入的优化
因为流插入和流提取的运算符重载都很简单,所以我们可以将他们变成内联函数,然后都放到头文件中。
内联函数,声明和定义都必须放在同文件中,不能将声明和定义分开。
class Date
{friend ostream& operator<<(ostream& out, const Date& d);friend istream& operator>>(istream& in, Date& d);
public:private:int _year;int _month;int _day;
};
inline ostream& operator<<(ostream& out, const Date& d)
{out << d._year << "年" << d._month << "月" << d._day << "日" << endl;return out;
}
inline istream& operator>>(istream& in, Date& d)
{in >> d._year >> d._month >> d._day;return in;
}
Ⅵ、 为什们流插入,流提取?
我们在C语言中,我们可以用
printf
,既可以打印内置类型,也可以打印自定义类型。
但是在C++中,我们用printf
确实可以打印内置类型,但是我们对于自定义类型也就是类,他的成员变量是私有,我们用printf
很难突破访问限定符的限制,所以我们就不能用printf
通过上面的自己构造自定义类型的流插入运算符重载,
发现我们可以按照一定的规则重载运算符<<
和>>
。
我们的自定义类型就能通过cout
,cin
打印,并且可以按照自己想要的方式打印。
五、六、取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义,编译器默认会生成。
我们自己实现取地址及const取地址操作符重载
using namespace std;class A
{
public:A* operator&(){return this;}const A* operator&() const{return this;}
private:int _a = 10;
};int main()
{const A aa;A x;cout << &x << endl;cout << &aa << endl;return 0;
}
所以我们就不需要写,我们直接用他们的默认成员函数。
类和对象的其他小知识点
一、 const
成员
我们先看一个例子
class A
{
public:void Print(){cout << _a << endl;}
private:int _a = 10;
};int main()
{const A aa;aa.Print();return 0;
}
我们的程序出现错误。其实这涉及到一个问题,权限的放到。
我们定义了一个对象aa
,但是我们把他设置为了const
,我们在实例化对象的时候会调用构造函数,我们传一个隐藏参数this
,指向aa
,但是*this
的类型却不是const
,所以造成了权限的放大。
所以要解决这个问题,就是将
this
设置为const
,
void Print() const
,规定const
修饰*this
.注意修饰的并不是this
class A
{
public:void Print() const{cout << _a << endl;}
private:int _a = 10;
};int main()
{const A aa;aa.Print();return 0;
}
所以我们以后内部不改变成员变量的成员函数就用
const
修饰,const
对象和普通对象都可以调用。
二、用运算符重载改变数组
class A
{
public:void Print() const{cout << _a << endl;}int& operator[](int i){assert(i < 10);return _a[i];}//函数重载const int& operator[](int i) const{assert(i < 10);return _a[i];}
private:int _a[10];
};int main()
{A x;for (int i = 0; i < 10; i++){x[i] = i * 2;}for (int i = 0; i < 10; i++){cout << x[i] << " ";}return 0;
}
三、构造函数的初始化列表
我们有两种初始化的方式
Ⅰ、第一种是构造函数体赋值(不相当于初始化)
虽然下面构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
class Date
{
public:Date(int year, int month, int day) {_year = year; _month = month; _day = day;}
private://声明int _year;int _month;int _day;
};int main()
{Date a;//对象整体的定义
}
Ⅱ、初始化列表
①引出初始化列表
我们知道对象实例化的时候(
Date a;
),是对象整体的定义。
这时就会有一个问题,每个成员什们时候定义的呢?
这时有人就会说,不就是在整体定义的时候就将其成员定义了吗?
其实话说是这样说,但是类好像并不是。
我举个例子
int main()
{//判断是否正确const int i;return 0;
}
我们看上面的代码,我们看他是典型的错误。
i
被const
修饰所以它具有常性,在定义完之后就不可在修改了,如果你不在定义的时候初始化,那么以后他将没有机会初始化了。
结论:
const
修饰的变量必须在定义的时候初始化。
那我们看下面的类
class A
{
public:private://这个只是声明,并不是定义const int _x;
};int main()
{A a;//对象整体的定义return 0;
}
首先上面这个程序是错误的。
对于这个类,成员变量是常变量,我们必须在定义的时候初始化,但是声明也不能初始化。对象整体的定义的时候也没有初始化。
所以就抛出了下面这个问题。
每个成员什们时候定义的呢?
- 第一种,其实我们在上面默认构造函数的时候说了,他对内置类型不做处理,其实是一个bug,在
C++11
,这时打了一个补丁。
用在声明的时候用缺省参数可以很好的解决问题。在调用默认构造函数的时候就用这个缺省参数定义了。具体看下面代码。
class A
{
public:private://用缺省参数const int _x = 1;
};int main()
{A a;//对象整体的定义return 0;
}
- 但是上面这种方法是在2011年,才打的补丁,C++在98年就出来,并且就有这个问题,那么在这段期间是这么解决的呢?
其实就是用到了初始化列表。
②怎么用初始化列表
因为上面的问题,所以必须给每个成员变量找一个定义的位置,否则就会像
const
的变量就没有办法初始化。
对于对象来说,初始化列表是成员变量定义的地方。
- 初始化列表的位置
- 在构造函数里面,具体位置看下面代码。 初始化列表形式
- 以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟 一个放在括号中的初始值或表达式。
class Date
{
public:Date(int year, int month, int day): _year(year), _month(month), _day(day){}
private:int _year;int _month;int _day;
};
③初始化列表的规定
- 那个对象调用构造函数,初始化列表是它所有成员变量定义的位置。
- 不管是否在初始化列表写,编译器的每个变量都会走初始化列表,在初始化列表定义和初始化。
做一下下面的练习题
class A
{
public:A():_x(1),_a2(1){_a1++;_a2++;}
private://用缺省参数int _a1 = 1;int _a2 = 2;const int _x;
};int main()
{A a;//对象整体的定义return 0;
}
答案是:
a1=2,a2=2,x=1;
。有了这个例子我相信大家肯定可以看懂第二点。
- 每个初始化列表只能出现一次。
class A
{
public:A():_x(1),_a2(1),_a2(2)//错误的代码不能出现两次,只能出现一次。{_a1++;_a2++;}
private://用缺省参数int _a1 = 1;int _a2 = 2;const int _x;
};
- 有三类必须在初始化列表初始化
①引用成员变量
②const成员变量
③自定义类型成员(且该类没有默认构造函数时)
对于前两个还比较好理解,引用如果不初始化,就会产生一个问题,他是谁的别名,这个就非常奇怪。
下来我们详细看一下为什们第三个不可以。
class B
{
public:B():b(1){}
private:int b;
};class A
{
public:A():_x(1),_a2(1)//引用的初始化,ret(_a1) //表示ret是_a1的引用。{_a1++;_a2++;}
private://用缺省参数int _a1 = 1;int _a2 = 2;const int _x;int& ret;B _bb;
};
首先上面这个程序没有任何问题
这时有人会问,为什们没有在初始化列表中让自定义类型初始化,怎么他还是对着呢?
我们第三点,规定的是没有默认构造的自定义类型,我们必须初始化。
但是类B
,它有默认构造函数(上面将默认构造函数写了:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数),我们对于自定义类型,会调用他的默认构造函数,将其初始化,这是默认构造函数对于自定义类型的特点。
所以这是没有问题的,B
类的对象_bb
调用自己的默认构造函数初始化了。
再看下面例子,判断是否正确。
class B
{
public:B(int):b(1){}
private:int b;
};class A
{
public:A():_x(1),_a2(1)//引用的初始化,ret(_a1) //表示ret是_a1的引用。{_a1++;_a2++;}
private://用缺省参数int _a1 = 1;int _a2 = 2;const int _x;int& ret;B _bb;
};int main()
{A a;//对象整体的定义return 0;
}
先说答案:上面的代码是错的,
对于自定义类型B
,他没有默认构造函数,但有构造函数。
因为它既不是全缺省,也不是无参构造函数,并且他写了,系统也不会自动生成默认构造函数。
既然它没有默认构造函数,那么他就不会自动调用默认构造函数,也就不会初始化。
所以我们在初始化列表中,我们必须将无默认构造函数的自定义类型初始化。
初始化过程看下面代码
class B
{
public:B(int):b(1){}
private:int b;
};class A
{
public:A():_bb(2) //可以随意给值,有了参数就可以调用它的构造函数。{}
private:B _bb;
};int main()
{A a;//对象整体的定义return 0;
}
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后 次序无关
利用这个知识点做下面这个题
class A
{
public:A(int a):_a1(a) ,_a2(_a1) {}void Print() {cout<<_a1<<" "<<_a2<<endl; }
private: int _a2; int _a1;
};
int main() { A aa(1); aa.Print();
}
答案:
a1 = 1
和a2 = 随机值
,我们在类的初始化的时候传的是1,调用它的构造函数,声明中我们先声明了a2
,所以在初始化列表中,我们先定义a2
,也就是_a2(_a1)
,但是这是a1
并没有初始化,所以a2
是随机值。
然后在定义a1
,将a1
初始化为1。
Ⅲ、explicit
关键字
我们通过代码来讲问题,这些都是为了引出explicit
关键字,大家先将下面的都行理解了。
class A
{
public:A(int a):_a1(a){}
private:int _a2;int _a1;
};int main()
{//构造函数A aa1(1);//隐式类型转换A aa = 1;
}
- 两个操作分别发生了什么?
- 第一个操作:
A aa1(1);
这个都不用想,肯定是构造函数。
但是第二个操作有点奇怪:A aa = 1
。
其实我们可以类比一下
int j = 0
; 和double i = j
;
我们利用C语言的知识,发生了隐式类型转换,
过程是这样的:将j
拷贝到一个临时变量,然后在转换为double
,最后赋值给i
。
其实第二个操作也是一样的,产生一个临时变量,这个临时变量的类型是
A*
,然后发生隐式类型转换,将1
转换为A*
,对临时变量发生构造函数,然后临时变量再对aa
进行拷贝构造函数。
所以总结一下,浅浅的提一下优化,下面会详细讲
第二步操作A aa = 1
是构造函数+拷贝构造。
但是我们的编译器会进行优化,将两步操作优化为一步构造。
总结构造 + 拷贝构造 + 优化 => 构造
explicit
作用
- 作用
- 用
explicit
修饰构造函数,将会禁止构造函数的隐式转换。
①单参数的构造函数
class A
{
public://单参数的构造函数explicit A(int a):_a1(a){}
private:int _a2;int _a1;
};int main()
{//构造函数A aa1(1);//隐式类型转换//这里会报错,禁止发生类型转换A aa = 1;
}
对于单参数的构造函数
当我们在构造函数前使用explicit
关键字,不能进行隐式转换
②多参数的构造函数
class A
{
public:A(int a1,int a2):_a1(a1),_a2(a2){}
private:int _a2;int _a1;
};int main()
{//多参数的构造函数//C++98 对多参数的类型转换不支持//C++11 开始支持的//构造函数A aa1(1, 1);//隐式类型转换A aa2 = { 2, 2 };
}
第一个还是我们的构造函数。
第二个是我们的隐式类型转换,但是是从C++11
才开始支持的。
如果我们不想隐式类型转换,我们就加上这个关键字
explicit
.
explicit A(int a1,int a2):_a1(a1),_a2(a2){}
Ⅳ、static成员
面试题
实现一个类,计算程序中创建出了多少个类对象。
//这是我们的类,计算主函数中,我们一共创建了几个类。
class A
{
public://构造函数A(int a = 0){}//拷贝构造A(const A&aa){}
};void Func(A a)
{}
int main()
{A aa1;A aa2(aa1);Func(aa1);A aa3 = 1;return 0;
}
分析:我们创造键一个类。
他只有两条路,一条是走构造函数,一条是走拷贝构造函数。
所以我们只要计算我们进行了几次构造函数和拷贝构造函数即可。
①方法一:创建一个全局变量。
//全局变量
int n = 0;
class A
{
public://构造函数A(int a = 0){n++;}//拷贝构造A(const A&aa){n++;}
};void Func(A a)
{}
int main()
{//构造函数一次A aa1;//拷贝构造一次A aa2(aa1);//进行了一次拷贝构造Func(aa1);//隐式类型转换优化为一次构造函数A aa3 = 1;cout << n << endl;return 0;
}
这种方式可行,但是这种方法的缺陷还是太明显,我们
n
是全局变量,我们随便可以随便修改,一改就变了。结果就会不对。
②方法二:static
成员
- 什么是
- 在类中用
static
修饰的成员变量,称之为静态成员变量;
静态成员变量一定要在类外进行初始化 注意 - 静态成员变量 ,不属于某个对象,属于所有对象,属于整个类. 为什们静态成员变量一定要在类外进行初始化?
- 我们在类中初始化的必须是类的对象,但是静态成员变量不属于某个对象,所以必须类外初始化。
static
成员?class A
{
public:
private://不属于某个对象,属于所有对象,属于整个类static int count;//声明
};int A::count = 0;//定义初始化
有了这个知识,我们还需要一个
GetCount()
函数获取count
,
具体代码如下
class A
{
public://构造函数A(int a = 0){count++;}//拷贝构造A(const A&aa){count++;}int GetCount(){//在这个函数中不受访问限定符是限制//如果在外面直接调用,就可以拿到count//只可以读,不可以写return count;}
private://不属于某个对象,属于所有对象,属于整个类static int count;//声明
};int A::count = 0;//定义初始化void Func(A a)
{}
int main()
{//构造函数一次A aa1;//拷贝构造一次A aa2(aa1);//进行了一次拷贝构造Func(aa1);//隐式类型转换优化为一次构造函数A aa3 = 1;A aa4[10];cout << aa3.GetCount() << endl;return 0;
}
但是有一个缺陷,他在最后调用的时候,只用用我们的创建的对象进行调用,这样是不是感觉很难受。
如果我们没有创建对象,想去调用好像没有办法
这是就有了静态成员函数。
③方法三:static
成员函数
我们想要我们的成员变量,必须先创建一个对象,再通过
Get
函数得到私有 的成员变量。
我们不想要我们的通过创建一个对象来调用函数从而得到数据,那我们有没有什么方法呢?==》其实我们的静态成员函数就可以很好的帮我们解决这个问题。
- 就是在成员函数前加一个
static
,就变成了静态成员函数。 - 就是没有
this
指针,我们知道了域名,就可以直接调用。
static
成员函数怎么用static
成员函数的特点可能有的人没有想明白,为什们没有this
指针,就可以直接调用,
我们知道,类中的成员函数都有一个隐藏的this
指针,可以帮我们指向对应的成员变量。
所以我们在传参的时候,我们必须传一个this
指针,就像 aa3.GetCount()
,它的this
指针就是aa3
对象的指针。
所以静态成员函数突破了这个限制,在传参的时候不传this
,就可以用域名找到。
- 答案肯定是不可以,但是我们要明白为什们不可以。
我们用我们的成员变量,都是用this
指针进行调用,但是我们上面说了静态成员函数没有this
,肯定就不可以调用呀,其实我们多想一想就可以明白。 什么时候用 - 当我们需要访问静态成员变量,并且可以用域名直接找到静态成员函数。
static
成员函数能不能调用我们的非静态成员变量static
成员函数class A
{
public://构造函数A(int a = 0){count++;}//拷贝构造A(const A&aa){count++;}//静态成员函数static int GetCount(){return count;}
private://不属于某个对象,属于所有对象,属于整个类static int count;//声明
};int A::count = 0;//定义初始化void Func(A a)
{}
int main()
{//构造函数一次A aa1;//拷贝构造一次A aa2(aa1);//进行了一次拷贝构造Func(aa1);//隐式类型转换优化为一次构造函数A aa3 = 1;A aa4[10];cout << A::GetCount() << endl;return 0;
}
Ⅴ、 匿名对象
- 匿名对象的样式
- 类名直接加括号。例如:上面类的匿名对象是
A()
。 匿名对象的特征 - 他没有名字,并且声明周期只在它这一行,一旦跳过这一行,直接析构。
举个例子
④方法四:用匿名对象调用,然后-1。
class A
{
public://构造函数A(int a = 0){count++;}//拷贝构造A(const A&aa){count++;}//int GetCount(){return count;}
private://不属于某个对象,属于所有对象,属于整个类static int count;//声明
};int A::count = 0;//定义初始化void Func()
{A aa4[10];
}
int main()
{Func();//这个就是匿名对象。cout << A().GetCount() -1 << endl;return 0;
}
Ⅵ、友元
- 友元
- 友元提供了一种突破封装的方式,有时提供了便利。
但是友元会增加耦合度,破坏了封装,所以 友元不宜多用。
友元分为:友元函数和友元类
① 友元函数
我们上面的流插入和流插入,基本将友元函数讲的差不多。能不用就不用。
友元函数的特征
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰(
const
修饰的this
,只能修饰非静态的成员函数,只有他们有this
) - 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
②友元类
他是一个但单向关系,你的是我的,我的还是我的。
对于下面这个
Date
是Time
的友元,所以Date
可以访问Time
,但是Time
不能访问Date
class Time
{friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类 中的私有成员变量
public:Time(int hour = 0, int minute = 0, int second = 0): _hour(hour), _minute(minute), _second(second){}
private:int _hour;int _minute;int _second;
};
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1): _year(year), _month(month), _day(day){}void SetTimeOfDate(int hour, int minute, int second){// 直接访问时间类私有的成员变量 _t._hour = hour;_t._minute = minute;_t._second = second;}
private:int _year;int _month;int _day;Time _t;
};
友元类的特征
- 友元关系是单向的,不具有交换性。
- 友元关系不能传递
- 如果C是B的友元, B是A的友元,则不能说明C时A的友元。
- 友元关系不能继承,在继承位置再给大家详细介绍。
Ⅶ、内部类
很简单,就是在类的里面再定义一个类。
class A
{
public://B就是内部类class B{private:int b = 2;};
private:int h =1;
};
拥有内部类的类的大小
计算上面A类的大小。
答案是:4.
所以我们可以初步判断一下,A类的大小只有他自己,并没有计算B。
内部类,其实和将类定义到全局没有区别。他是独立的
用上面举个例子,类B是类A的内部类,但是类B是独立的,只是受域的限制,必须再A域中才能找到B。
还有一个特点,B天生就是A的友元,B可以访问A,但是A不能访问B。
Ⅷ、编译器的优化
优化的过程都在代码中的注释,大家好好研究
class A
{
public://构造函数A(int a = 0):_a(a){cout << "A(int a)" << endl;}//拷贝构造A(const A& aa):_a(aa._a){cout << "A(const A& aa)" << endl;}//运算符重载A& operator=(const A& aa){cout << "A& operator=(const A& aa)" << endl;if (this != &aa){_a = aa._a;}return *this;}//析构函数~A(){cout << "~A()" << endl;}
private:int _a;
};void fun(A aa)
{}
void func(const A& aa)
{}
int main()
{//隐式类型转化//构造+拷贝构造+优化为==>构造A aa1 = 1;//传值传参//拷贝构造,没有优化fun(aa1);//构造+拷贝构造+优化为==>构造fun(2);//构造+拷贝构造+优化为==>构造fun(A(3));//引用传参//没有优化,啥都不调用func(aa1);//无优化,隐式转换为直接引用func(2);//无优化 就是构造完直接引用func(A(3));return 0;
}
再来一种形式
A function()
{//进行构造函数A aa;//在返回的时候进行拷贝构造return aa;
}
int main()
{function();//function()函数中一个构造,一个拷贝构造//然后再一个拷贝构造//被优化为一个构造+拷贝构造。A aa1 = function();
}
再来一种形式
A function2()
{//直接返回一个匿名对象。//进行一个构造函数+拷贝构造//被优化为一个构造return A();
}
int main()
{function2();//上面说函数被优化为一个构造//在下面那就进行一个构造+拷贝构造//被优化为==>一个构造A aa1 = function2();
}
优化总结
- 对象返回总结
-
- 接受返回值对象,尽量拷贝构造方式接收,不要赋值接收
-
- 函数中返回对象时,尽量返回匿名对象
函数传参优化
- 函数中返回对象时,尽量返回匿名对象
- 尽量使用
const &
传参。也就是引用传参。
知识总结加训练
写一个日期类
还是我们要创建一个项目。
还是分成三个文件
Date.h
,将我们的声明,以及头文件包含。
Date.cpp
函数的实现。
test.cpp
测试我们这个类
Date.h
#pragma once#include<iostream>
#include<assert.h>
using namespace std;
class Date
{friend ostream& operator<<(ostream& out, const Date& d);friend istream& operator>>(istream& in, Date& d);
public:Date(int year = 1900, int month = 1, int day = 1);int GetMonthDay(int year, int month);void Print();bool operator==(const Date& d);bool operator < (const Date& d);bool operator <= (const Date& d);bool operator > (const Date& d);bool operator >= (const Date& d);bool operator != (const Date& d);Date& operator=(const Date& d);Date& operator+=(int day);Date operator+(int day);Date& operator-=(int day);//d1-100Date operator-(int day);//d1-d2int operator-(const Date& d);//前置加加 ++d1Date& operator++(); //编译器自动匹配//后置加加 d1++//参数是为了构成函数重载//规定必须是整型,可以不加参数//仅仅是为了占位,跟前置重载区分Date operator++(int); //编译器自动匹配//前置减减 --d1 ->d1.operator--()Date& operator--();//后置减减 d1-- ->d1.operator--(1)Date operator--(int); //编译器自动匹配输出流//void operator<<(ostream& out);private:int _year;int _month;int _day;
};inline ostream& operator<<(ostream& out, const Date& d)
{out << d._year << "年" << d._month << "月" << d._day << "日" << endl;return out;
}inline istream& operator>>(istream& in, Date& d)
{in >> d._year >> d._month >> d._day;return in;
}
Date.cpp
#define _CRT_SECURE_NO_WARNINGS 1 #include"Date.h"
int Date::GetMonthDay(int year, int month)
{assert(month > 0 && month < 13);int monthArray[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;}else{return monthArray[month];}
}Date::Date(int year, int month , int day )
{if (month > 0 && month < 13&& day>0 && day <= GetMonthDay(year, month)){_year = year;_month = month;_day = day;}else {cout << "日期非法" << endl;}}void Date::Print()
{cout << _year << "/" << _month << "/" << _day << "/" << endl;
}
bool Date::operator==(const Date& d)
{return _year == d._year &&_month == d._month &&_day == d._day;
}
bool Date::operator < (const Date& d)
{if (_year < d._year){return true;}else if (_year == d._year && _month < d._month){return true;}else if (_year == d._year && _month == d._month && _day < d._day){return true;}else{return false;}
}
bool Date::operator <= (const Date& d)
{return *this < d || *this == d;
}bool Date::operator > (const Date& d)
{return !(*this <= d);
}bool Date::operator >= (const Date& d)
{return !(*this < d);
}bool Date::operator != (const Date& d)
{return !(*this == d);
}Date& Date::operator=(const Date& d)
{if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;}//返回值是为了连续赋值。
Date& Date::operator+=(int day)
{if (day < 0){*this -= -day;return *this;}_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 tmp(*this);
// tmp._day += day;
// while (tmp._day > GetMonthDay(tmp._year, tmp._month))
// {
// tmp._day -= GetMonthDay(tmp._year, tmp._month);
// tmp._month++;
// if (tmp._month == 13)
// {
// ++tmp._year;
// tmp._month = 1;
// }
// }
// return tmp;
//}Date Date::operator+(int day)
{Date tmp(*this);tmp += day;return tmp;
}//前置加加 ++d1
Date& Date::operator++()
{*this += 1;return *this;
}//后置加加 d1++
Date Date::operator++(int)
{Date tmp(*this);*this += 1;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;
}//d1-100
Date Date:: operator-(int day)
{Date tmp(*this);tmp -= day;return tmp;
}//前置减减 --d1 ->d1.operator--()
Date& Date::operator--()
{*this -= 1;return *this;
}
//后置减减 d1-- ->d1.operator--(1)
Date Date::operator--(int)//编译器自动匹配
{Date tmp(*this);*this -= 1;return tmp;
}//d1-d2
int Date::operator-(const Date& d)
{Date max = *this;Date min = d;int flag = 1;if (*this < d){max = d;min = *this;flag = -1;}//int n = 0;while (min != max){++min;++n;}return n * flag;
}输出流
//void Date::operator<<(ostream& out)
//{
// out << _year << "年" << _month << "月" << _day << "日"<< endl;
//}//ostream& operator<<(ostream& out,const Date& d)
//{
// out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
//
// return out;
//}
//
//
//istream& operator>>(istream& in, Date& d)
//{
//
// in >> d._year >> d._month >> d._day;
// return in;
//}
总结
我这篇博客几乎包括了类和对象的所有东西,如果你真的看懂了,说明你入门了,加油!!!!
相关文章:

C++入门 谁都能看懂的类和对象
类 C语言结构体中只能定义变量. 在C中,结构体内不仅可以定义变量,也可以定义函数。 //c语言 typedef struct ListNode {int val;struct ListNode* next; }LTN; //c struct ListNode {int val;//c中可以直接用这个,不用加structListNode* next…...

C++ STL:string类的模拟实现
目录 前置说明 一. 构造函数和析构函数的模拟实现 1.1 构造函数 1.2 析构函数 二. string类对象容量及成员相关的函数 2.1 获取字符串有效字符数、容量及_str成员变量获取相关函数 2.2 扩容及变长相关函数 2.3 字符串清空和判空函数 三. 运算符重载函数 3.1 赋值运算…...

并发编程---线程池(六)
阻塞队列的应⽤——线程池一 线程池基本概念二 线程池三种常⽤创建⽅式2.1.newFixedThreadPool线程池:2.2.newSingleThreadExecutor线程池:2.3.newCachedThreadPool线程池:2.4. 线程池代码演示三 线程池创建的七个参数四 线程池底层原理理解&…...
【Java实战】不会还有人用if else进行参数校验吧
当请求参数很多,几乎每一个参数都需要后端去兜底校验时,你还在写if else去判断参数是否为空吗??要校验为空的参数三四个还好,要是十几个,业务逻辑还没开始就写二三十行代码开始堆山了嘛,教给大家…...
深度学习部署(十六): CUDA RunTime API _vector-add 使用cuda核函数实现向量加法
1. 知识点 nthreads的取值,不能大于block能取值的最大值。一般可以直接给512、256,性能就是比较不错的 (input_size block_size - 1) / block_size;是向上取整 对于一维数组时,采用只定义layout的x维度,若处理的是二维ÿ…...
堆结构的两个应用
堆排序 堆结构很大的一个用处,就是用于堆排序了,堆排序的时间复杂度是O(n∗log2n)O(n*log_2n)O(n∗log2n)量级的,在众多排序算法中所处的地位也是高手级别的了。 但很多人在使用堆排序的时候,首先认为我必须得有一个堆数据结构…...
Java中的 static
1 static 静态变量 1.1 静态变量的使用 static变量也称作静态变量,也叫做类变量 静态变量被所有的对象所共享,在内存中只有一个副本 当且仅当在类初次加载时会被初始化 静态变量属于类 通过类名就可以直接调用静态变量 也可以通过对象名.静态变量…...

基于Vision Transformer的图像去雾算法研究与实现(附源码)
基于Vision Transformer的图像去雾算法研究与实现 0. 服务器性能简单监控 \LOG_USE_CPU_MEMORY\文件夹下的use_memory.py文件可以实时输出CPU使用率以及内存使用率,配合nvidia-smi监控GPU使用率 可以了解服务器性能是否足够;运行时在哪一步使用率突然…...
服务器相关常用的命令
cshell语法 https://www.doc88.com/p-4985161471426.html domainname命令 1)查看当前系统域名 domainname2)设置并查看当前系统域名 domainname example.com3)显示主机ip地址 domainname -Iwhich命令 which 系统命令在 PATH 变量指定的…...

今天是国际数学日,既是爱因斯坦的生日又是霍金的忌日
目录 一、库函数计算 π 二、近似值计算 π 三、无穷级数计算 π 四、割圆术计算 π 五、蒙特卡罗法计算 π 六、计算800位精确值 从2020年开始,每年的3月14日又被定为国际数学日,是2019年11月26日联合国教科文组织第四十届大会上正式宣布…...

Qt Quick - StackLayout 堆布局
StackLayout 堆布局一、概述二、attached 属性三、例子1. 按钮切换 StackLayout 页面一、概述 StackLayout 其实就是说,在同一个时刻里面,只有一个页面是展示出来的,类似QStackWidget 的功能,主要就是切换界面的功能。这个类型我…...

C/C++网络编程笔记Socket
https://www.bilibili.com/video/BV11Z4y157RY/?vd_sourced0030c72c95e04a14c5614c1c0e6159b上面链接是B站的博主教程,源代码来自上面视频,侵删,这里只是做笔记,以供复习和分享。上一篇博客我记录了配置环境并且跑通了࿰…...

RK3568平台开发系列讲解(网络篇)什么是Socket套接字
🚀返回专栏总目录 文章目录 一、什么是socket ?二、socket 理解为电话机三、socket 的发展历史四、套接字地址格式4.1、通用套接字地址格式4.2、IPv4 套接字格式地址4.3、IPv6 套接字地址格式4.4、几种套接字地址格式比较沉淀、分享、成长,让自己和他人都能有所收获!😄 …...
2022年全国职业院校技能大赛(中职组)网络安全竞赛试题——渗透测试解析(详细)
渗透测试 任务环境说明: 服务器场景:Server9服务器场景操作系统:未知(关闭连接)系统用户名:administrator密码:123456通过本地PC中渗透测试平台Kali对靶机场景进行系统服务及版本扫描渗透测试,以xml格式向指定文件输出信息(使用工具Nmap),将以xml格式向指定文件输出…...

尚融宝03-mybatis-plus基本CRUD和常用注解
目录 一、通用Mapper 1、Create 2、Retrieve 3、Update 4、Delete 二、通用Service 1、创建Service接口 2、创建Service实现类 3、创建测试类 4、测试记录数 5、测试批量插入 三、自定义Mapper 1、接口方法定义 2、创建xml文件 3、测试条件查询 四、自定义Serv…...

vue多行显示文字展开
这几天项目里面有一个需求,多行需要进行展开文字,类似实现这种效果 难点就在于页面布局 一开始就跟无头苍蝇似的,到处百度 ,后面发现网上的都不适合自己,最终想到了解决方案 下面是思路: 需求是超过3行&a…...

SpringBoot:SpringBoot 的底层运行原理解析
声明原文出处:狂神说 文章目录1. pom.xml1 . 父依赖2 . 启动器 spring-boot-starter2. 主启动类的注解1. 默认的主启动类2. SpringBootApplication3. ComponentScan4. SpringBootConfiguration5. SpringBootApplication 注解6. spring.factories7. 结论8. 简单图解3…...

哪些场景会产生OOM?怎么解决?
文章目录 堆内存溢出方法区(运行时常量池)和元空间溢出直接内存溢出栈内存溢出什么时候会抛出OutOfMemery异常呢?初看好像挺简单的,其实深究起来考察的是对整个JVM的了解,而这个问题从网上可以翻到一些乱七八糟的答案,其实在总结下来基本上4个场景可以概括下来。 堆内存溢出…...

金三银四、金九银十 面试宝典 Spring、MyBatis、SpringMVC面试题 超级无敌全的面试题汇总(超万字的面试题,让你的SSM框架无可挑剔)
Spring、MyBatis、SpringMVC 框架 - 面试宝典 又到了 金三银四、金九银十 的时候了,是时候收藏一波面试题了,面试题可以不学,但不能没有!🥁🥁🥁 一个合格的 计算机打工人 ,收藏夹里…...

JAVA开发(Spring框架详解)
javaweb项目几乎已经离不开spring框架了,spring 是一个典型的分层架构框架,它包含一系列的功能并被分为多个功能模块,springboot对spring框架又做了一层封装,以至于很多人对原来的spring框架越来越不了解。 要谈Spring的历史&…...

树莓派超全系列教程文档--(61)树莓派摄像头高级使用方法
树莓派摄像头高级使用方法 配置通过调谐文件来调整相机行为 使用多个摄像头安装 libcam 和 rpicam-apps依赖关系开发包 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 配置 大多数用例自动工作,无需更改相机配置。但是,一…...

简易版抽奖活动的设计技术方案
1.前言 本技术方案旨在设计一套完整且可靠的抽奖活动逻辑,确保抽奖活动能够公平、公正、公开地进行,同时满足高并发访问、数据安全存储与高效处理等需求,为用户提供流畅的抽奖体验,助力业务顺利开展。本方案将涵盖抽奖活动的整体架构设计、核心流程逻辑、关键功能实现以及…...
【论文笔记】若干矿井粉尘检测算法概述
总的来说,传统机器学习、传统机器学习与深度学习的结合、LSTM等算法所需要的数据集来源于矿井传感器测量的粉尘浓度,通过建立回归模型来预测未来矿井的粉尘浓度。传统机器学习算法性能易受数据中极端值的影响。YOLO等计算机视觉算法所需要的数据集来源于…...
关于 WASM:1. WASM 基础原理
一、WASM 简介 1.1 WebAssembly 是什么? WebAssembly(WASM) 是一种能在现代浏览器中高效运行的二进制指令格式,它不是传统的编程语言,而是一种 低级字节码格式,可由高级语言(如 C、C、Rust&am…...

ArcGIS Pro制作水平横向图例+多级标注
今天介绍下载ArcGIS Pro中如何设置水平横向图例。 之前我们介绍了ArcGIS的横向图例制作:ArcGIS横向、多列图例、顺序重排、符号居中、批量更改图例符号等等(ArcGIS出图图例8大技巧),那这次我们看看ArcGIS Pro如何更加快捷的操作。…...

深度学习习题2
1.如果增加神经网络的宽度,精确度会增加到一个特定阈值后,便开始降低。造成这一现象的可能原因是什么? A、即使增加卷积核的数量,只有少部分的核会被用作预测 B、当卷积核数量增加时,神经网络的预测能力会降低 C、当卷…...

2025季度云服务器排行榜
在全球云服务器市场,各厂商的排名和地位并非一成不变,而是由其独特的优势、战略布局和市场适应性共同决定的。以下是根据2025年市场趋势,对主要云服务器厂商在排行榜中占据重要位置的原因和优势进行深度分析: 一、全球“三巨头”…...

安宝特方案丨船舶智造的“AR+AI+作业标准化管理解决方案”(装配)
船舶制造装配管理现状:装配工作依赖人工经验,装配工人凭借长期实践积累的操作技巧完成零部件组装。企业通常制定了装配作业指导书,但在实际执行中,工人对指导书的理解和遵循程度参差不齐。 船舶装配过程中的挑战与需求 挑战 (1…...

视频行为标注工具BehaviLabel(源码+使用介绍+Windows.Exe版本)
前言: 最近在做行为检测相关的模型,用的是时空图卷积网络(STGCN),但原有kinetic-400数据集数据质量较低,需要进行细粒度的标注,同时粗略搜了下已有开源工具基本都集中于图像分割这块,…...
return this;返回的是谁
一个审批系统的示例来演示责任链模式的实现。假设公司需要处理不同金额的采购申请,不同级别的经理有不同的审批权限: // 抽象处理者:审批者 abstract class Approver {protected Approver successor; // 下一个处理者// 设置下一个处理者pub…...