C++——类和对象(全)

1.类的定义

1.1类定义格式

class Stack

{};

1)class为定义类的关键字 Stack为类的名字,{};中为类的主体(分号不能省略)。

2)类体中内容称为类的成员:类中的变量称为类的属性或成员变量;类中函数称为类的方法或成员函数;

3)为了区分成员变量,一般在成员变量会加上特殊标识_或则m开头。

4)C++struct也可以定义类,兼容struct,升级为类,明显变化是,struct中可以定义函数,但一般情况下,还是建议在class定义类。

5)定义在类面的成员函数默认为inline。用inline修饰的函数叫做内联函数,编译时c++编译器会在调用的地方展开内联函数,这样调用内联函数就需要建立栈帧,可以提高效率。

#include<iostream>
using namespace std;
inline int Add(int x, int y)
{
int ret = x + y;
ret += 1;
ret += 1;
ret += 1;
return ret;
} 
int main()
{
// 可以通过汇编观察程序是否展开
// 有call Add语句就是没有展开,没有就是展开了
int ret = Add(1, 2);
cout << Add(1, 2) * 5 << endl;
return 0;
}

1.2 访问限定符

1)C++一种实现封装的方式,用类将对象的属性与方法结合在一起,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。

2)public修饰的成员在类外可以直接被访问;protected和private修饰的成员不能再类外直接被访问。

3)访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。若没有,就是到 } 结束。

4)class定义成员没有被访问限定符修饰默认为private ,struct 默认为public。

5)一般成员变量都会被限制为private/protevted,需要给别人使用的成员函数会放为public。

1.3 类域

1)类定义另一个新的作用域,类的所有成员都在类的作用域中,再类体外定义成员时,要使用 :: 作用域操作符指明成员属于哪个类域。

2)类域影响到的是编译查找规则,下面Init函数如果不指定类域Stack,那么编译器就把Init当成全局函数,编译时就找不到array等成员的声明/定义在哪里,就会报错。指定类域Stack,就知道Init是成员函数,当前域找不到array等成员,就回去类域中查找。

#include<iostream>
using namespace std;
class Stack
{ 
public:
// 成员函数
void Init(int n = 4);
private:
// 成员变量
int* array;
size_t capacity;
size_t top;
};
// 声明和定义分离,需要指定类域
void Stack::Init(int n)
{
array = (int*)malloc(sizeof(int) * n);
if (nullptr == array)
{
perror("malloc申请空间失败");
return;
} 
capacity = n;
top = 0;
} 
int main()
{
Stack st;
st.Init();
return 0;
}

2.实例化

2.1实例化概念

1)用类类型在物理内存中创建对象的过程,称为实例化出对象。

2)类是对象进行的一种抽象描述,是一个模型一样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象是,才会分配空间。

3)一个类可以实例化出多个对象,实例化出的对象,占用实际的物理空间,存储类成员变量。打个⽐⽅:类实例化出对象就像现实中使⽤建筑设计图建造出房⼦,类就像是设计图,设计图规划了有多少个房间,房间⼤⼩功能等,但是并没有实体的建筑存在,也不能住⼈,⽤设计图修建出房⼦,房⼦才能住⼈。同样类就像设计图⼀样,不能存储数据,实例化出的对象分配物理内存存储数据。

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;
};

2.2对象大小

分析⼀下类对象中哪些成员呢?类实例化出的每个对象,都有独⽴的数据空间,所以对象中肯定包含成员变量,那么成员函数是否包含呢?⾸先函数被编译后是⼀段指令,对象中没办法存储,这些指令存储在⼀个单独的区域(代码段),那么对象中⾮要存储的话,只能是成员函数的指针。再分析⼀下,对象中是否有存储指针的必要呢,Date实例化d1和d2两个对象,d1和d2都有各⾃独⽴的成员变量_year/_month/_day存储各⾃的数据,但是d1和d2的成员函数Init/Print指针却是⼀样的,存储在对象中就浪费了。如果⽤Date实例化100个对象,那么成员函数指针就重复存储100次,太浪费了。这⾥需要再额外哆嗦⼀下,其实函数指针是不需要存储的,函数指针是⼀个地址,调⽤函数被编译成汇编指令[call 地址], 其实编译器在编译链接时,就要找到函数的地址,不是在运⾏时找,只有动态多态是在运⾏时找,就需要存储函数地址。

c++规定类实例化对象也要符合内存对齐规则:

1)第一个成员在与结构体偏移量为0的地址处。

2)其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。

VS默认对齐数是8.

3)结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。

4)如果嵌套了结构体的情况,嵌套的结构体对⻬到⾃⼰的最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体的对⻬数)的整数倍。

上一个例题eg:

#include<iostream>
using namespace std;
// 计算⼀下A/B/C实例化的对象是多⼤?
class A
{ 
public:
void Print()
{
cout << _ch << endl;
}
private:
char _ch;
int _i;
};
class B
{ 
public:
void Print()
{
//...
}
};
class C
{};
int main()
{
A a;
B b;
C c;
cout << sizeof(a) << endl;
cout << sizeof(b) << endl;
cout << sizeof(c) << endl;
return 0;
}

上⾯的程序运⾏后,我们看到没有成员变量的B和C类对象的⼤⼩是1,为什么没有成员变量还要给1个字节呢?因为如果⼀个字节都不给,怎么表⽰对象存在过呢!所以这⾥给1字节,纯粹是为了占位标识对象存在。

3.this指针

1)Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调⽤Init和Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?那么这⾥就要看到C++给了⼀个隐含的this指针解决这⾥的问题。

2)编译器编译后,类的成员函数默认都会在形参第⼀个位置,增加⼀个当前类类型的指针,叫做this指针。⽐如Date类的Init的真实原型为, void Init(Date* const this, int year,int month, int day)

3)类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给_year赋值,

this->_year = year;

4)C++规定不能在实参和形参的位置显⽰的写this指针(编译时编译器会处理),但是可以在函数体内显⽰使⽤this指针。

class Date
{ 
public:
// void Init(Date* const this, int year, int month, int day)
void Init(int year, int month, int day)
{
// 编译报错:error C2106: “=”: 左操作数必须为左值
// this = nullptr;
// this->_year = year;
_year = year;
this->_month = month;
this->_day = day;
} 
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
// 这⾥只是声明,没有开空间
int _year;
int _month;
int _day;
};


this指针存在栈区,算对象大小的时候,就没有计算this指针大小,就说明不在对象内。

1.下⾯程序编译运⾏结果是()

A、编译报错 B、运⾏崩溃 C、正常运⾏

#include<iostream>
using namespace std;
class A
{
 public:
void Print()
{
cout << "A::Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}

这题会正常运行,选c,因为p就是指向类A的指针,在被创建的时候,这个地址就已经被存到汇编ecx中,并不需要解引用,也不需要用到this指针,因为p就是代表指针,当被置为空后,任然能找到Print();

2.下⾯程序编译运⾏结果是()

A、编译报错 B、运⾏崩溃 C、正常运⾏

#include<iostream>
using namespace std;
class A
{ 
public:
void Print()
{
cout << "A::Print()" << endl;
cout << _a << endl; //在这里就会解引用,用到this指针去访问类的对象,但是p已经被设为空指针,运行会崩溃
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}

//在这里就会解引用,用到this指针去访问类的对象,但是p已经被设为空指针,运行会崩溃。

4.c++和c实现stack对比

c++封装函数在一个stack类中,类外不能访问类中的私有对象,体现封装的思想,安全可靠。

c就比较随意,struct结构体来定义,但是结构体外却可以随时访问成员,不安全。

面向对象三大特性:封装,继承,多态。

5.类的默认成员函数

6.构造函数(实际是去初始化,完美替代了Init())

构造函数的特点:

1. 函数名与类名相同

2. ⽆返回值。 (返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)

3. 对象实例化时系统会⾃动调⽤对应的构造函数。

4. 构造函数可以重载

5. 如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦⽤⼾显式定义编译器将不再⽣成。(全缺省 半缺省 不缺省)

6. ⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。⽆参构造函数和全缺省构造函数虽然构成函数重载,但是调⽤时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认⽣成那个叫默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调⽤的构造就叫默认构造。

7. 我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤初始化列表才能解决,初始化列表,我们下个章节再细细讲解。

class Date
{
 public:
// 1.⽆参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
} 
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
} 
// 3.全缺省构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
}

7.析构函数

析构函数与构造函数功能相反,析构函数不是完成对对象本⾝的销毁,⽐如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会⾃动调⽤析构函数,完成对象中资源的清理释放⼯作。析构函数的功能类⽐我们之前Stack实现的Destroy功能,⽽像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。

析构函数的特点:

1. 析构函数名是在类名前加上字符 ~。

2. ⽆参数⽆返回值。 (这⾥跟构造类似,也不需要加void)

3. ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数。

4. 对象⽣命周期结束时,系统会⾃动调⽤析构函数。

5. 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数。

6. 还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类型成员⽆论什么情况都会⾃动调⽤析构函数。(自定义类型的成员不需要管析构,系统会自动调用默认的)

7. 如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数,如Date;如果默认⽣成的析构就可以⽤,也就不需要显⽰写析构,如MyQueue;但是有资源申请时,⼀定要⾃⼰写析构,否则会造成资源泄漏,如Stack。

8. ⼀个局部域的多个对象,C++规定后定义的先析构。

8.拷贝构造(拷贝构造,没有返回值,跟构造函数一样)

拷贝构造函数的作用是使用一个已存在的对象来初始化一个新创建的对象。

如果⼀个构造函数的第⼀个参数是⾃⾝类类型的引⽤,且任何额外的参数都有默认值,则此构造函数也叫做拷⻉构造函数,也就是说拷⻉构造是⼀个特殊的构造函数。

拷⻉构造的特点:

1. 拷⻉构造函数是构造函数的⼀个重载。

2. 拷⻉构造函数的第⼀个参数必须是类类型对象的引⽤,使⽤传值⽅式编译器直接报错,因为语法逻辑上会引发⽆穷递归调⽤。 拷⻉构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引⽤,后⾯的参数必须有缺省值。

3. C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥⾃定义类型传值传参和传值返回都会调⽤拷⻉构造完成

4. 若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉(包括地址,同一块空间)),对⾃定义类型成员变量会调⽤他的拷⻉构造。

5. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完成需要的拷⻉,所以不需要我们显⽰实现拷⻉构造。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源(因为当拷贝时,会发生二次析构导致程序崩溃),编译器⾃动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉(就是拷贝和被拷贝的内容都被两个指针同时指向 发生二次析构)不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的拷⻉构造会调⽤Stack的拷⻉构造,也不需要我们显⽰实现MyQueue的拷⻉构造。这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就需要显⽰写拷⻉构造,否则就不需要。 

6. 传值返回会产⽣⼀个临时对象调⽤拷⻉构造,传值引⽤返回,返回的是返回对象的别名(引⽤),没有产⽣拷⻉。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以减少拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回。

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//Date d2(d1);
	//拷贝构造就是构造函数的重载
	Date(const Date& d) //拷贝构造,为了防止死循环,就一定要加引用,传值就是一种拷贝构造
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

private:
	int _year;
	int _month;
	int _day;
};

9. 赋值运算符重载

9.1 运算符重载

• 当运算符被⽤于类类型的对象时,C++语⾔允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载,若没有对应的运算符重载,则会编译报错。

• 运算符重载是具有特殊名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体。

• 重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数。

• 如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数⽐运算对象少⼀个。

class Date
{
    //operator 在类内,那么第一个传参给this
    bool operator == (Date d2)
   {
       return _year == d2._year
           && _month == d2._month
           && _day == d2._day;
   {

};

int main()
{
    Date x1(2024,7,10);
    Date x2(2024,7,11);
    
    x1.operator == (x2);
    //相当于
    x1 == x2;

    return 0;
}

• 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。

• 不能通过连接语法中没有的符号来创建新的操作符:⽐如operator@。

•  .   *    ::    sizeof    ? :    . 注意以上5个运算符不能重载。(选择题⾥⾯常考,⼤家要记⼀下)

• 重载操作符⾄少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: intoperator+(int x, int y)

• ⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义,⽐如Date类重载operator-就有意义,但是重载operator+就没有意义。

• 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,⽆法很好的区分。C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,⽅便区分。

• 重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调⽤时就变成了 对象<<cout,不符合使⽤习惯和可读性。重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象。

// 编译报错:“operator +”必须⾄少有⼀个类类型的形参
int operator+(int x, int y)
{
return x - y;
}


bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
i
nt main()
{
Date d1(2024, 7, 5);
Date d2(2024, 7, 6);
// 运算符重载函数可以显⽰调⽤
operator==(d1, d2);  //此时的 == 就是一个函数名

// 编译器会转换成 operator==(d1, d2);
d1 == d2;
return 0;
}

9.2 赋值运算符重载

赋值运算符重载是⼀个默认成员函数,⽤于完成两个已经存在的对象直接的拷⻉赋值,这⾥要注意跟拷⻉构造区分,拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象。

赋值运算符重载的特点:

1. 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成const 当前类类型引⽤,否则会传值传参会有拷⻉

2. 有返回值,且建议写成当前类类型引⽤,引⽤返回可以提⾼效率,有返回值⽬的是为了⽀持连续赋值场景。

3. 没有显式实现时,编译器会⾃动⽣成⼀个默认赋值运算符重载,默认赋值运算符重载⾏为跟默认拷⻉构造函数类似,对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的赋值重载函数。

4. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的赋值运算符重载就可以完成需要的拷⻉,所以不需要我们显⽰实现赋值运算符重载。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器⾃动⽣成的赋值运算符重载完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的赋值运算符重载会调⽤Stack的赋值运算符重载,也不需要我们显⽰实现MyQueue的赋值运算符重载。这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就需要显⽰写赋值运算符重载,否则就不需要。

// 传引⽤返回减少拷⻉
// d1 = d2;
Date& operator=(const Date& d)
{
// 不要检查⾃⼰给⾃⼰赋值的情况
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
} 
// d1 = d2表达式的返回对象应该为d1,也就是*this
return *this;
}



// 需要注意这⾥是拷⻉构造,不是赋值重载
// 请牢牢记住赋值重载完成两个已经存在的对象直接的拷⻉赋值
// ⽽拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象
Date d4 = d1;

一个简单的日期计算作业:

#include <iostream>
#include <stdbool.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)
	{
		_year = year;
		_month = month;
		_day = day;

		if (CheckDate())
		{
			cout << "非法日期";
			Print();
			cout << endl;
		}
	}

	bool CheckDate();

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

	//放在类里面,默认是inline内联
	int GetMonthDay(int year, int month)
	{
		assert(month > 0 && month < 13);
		static int monthDayArray[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 monthDayArray[month];
	}

	//+=
	Date& operator+=(int day)
	{
		if (day < 0)
		{
			return (*this) -= -day;
		}

		_day += day;
		while (_day > GetMonthDay(_year,_month))
		{
			_day -= GetMonthDay(_year, _month);
			++_month;
			if (_month == 13)
			{
				++_year;
				_month = 1;
			}
		}

		return *this;
	}


	Date operator+(int day)
	{
		//复用+=
		Date tmp = *this;
		if (day < 0)
		{
			return tmp -= -day;
		}

		return tmp += 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;//tmp是一个局部对象,出了作用域就被销毁了,不能&返回
	}

	//-=
	Date& operator-=(int day);

	//-
	Date operator-(int day);

	//<
	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);

	//d1++
	//d1.operator(0)
	Date operator++(int);

	//++d1
	//d1.operator()
	Date operator++();

	//日期相减
	int operator-(const Date& d);

	//只能放到全局函数,来符合用户习惯,防止第一个永远是this
	重载输入流<<
	//void operator<<(ostream& out);

	重载输入流<<
	//void operator>>(ostream& out);

private:
	int _year;
	int _month;
	int _day;
};

//检查日期
bool Date::CheckDate()
{
	if (_month>=1&&_month<=12&&_day > Date::GetMonthDay(_year, _month))
	{
		return true;
	}
	else return false;
}

Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		return *this += -day;
	}

	_day -= day;
	while (_day <= 0)
	{
		--_month;
		if (_month == 0)
		{
			--_year;
			_month = 12;
		}
		_day += GetMonthDay(_year, _month);
		
	}

	return *this;
}

Date Date::operator-(int day)
{
	Date tmp = *this;
	if (day < 0)
	{
		return tmp += -day;
	}

	tmp -= day;
	return tmp;
}

//比较大小 d1<d2
bool Date::operator<(const Date& d)
{
	if (_year < d._year)
	{
		return true;
	}
	else if(_year == d._year)
	{
		if (_month < d._month)
		{
			return true;
		}
		else if (_month == d._month)
		{
			return _day < d._day;
		}
	}

	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 _year == d._year
		&& _month == d._month
		&& _day == d._day;
}

//!=
bool Date::operator!=(const Date& d)
{
	return !(*this == d);
}

//d1++
Date Date::operator++(int)
{
	Date tmp = *this;
	*this += 1;
	return tmp;
}

//++d1
Date Date::operator++()
{
	return ((*this) += 1);
}

//日期-日期
int Date::operator-(const Date& d)
{
	if (*this >= d)
	{
		Date tmp = d;
		int count = 0;
		while (tmp != *this)
		{
			tmp += 1;
			count += 1;
		}
		return count;
	}
	else return false;
}

//<< >> 重载时要变成全局函数
//重载输出流<<     cout<<d1<<d2 就相当于从cout<<d1 是从左往右结合
//流对象不支持拷贝,所以ostream& 必须要加引用返回
ostream&  operator<<(ostream& out,const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日";
	return out;//就可以保证是左边的返回值,保证连续输出
}

//重载输入流>>
istream& operator>>(istream& in, Date& d)
{
	while (1)
	{
		cout << "请依次输入年月日:>";
		in >> d._year >> d._month >> d._day;
		if (d.CheckDate())
		{
			cout << "日期非法,请重新输入"<<endl;
		}
		else break;
	}
	return in;
}

int main()
{
	Date d1(2024, 8, 8);
	Date d4(2024, 8, 30);
	Date d2 = d1 + 1000;
	Date d3(d1 + 10000);

	cin >> d1 >> d2;
	cout << d2 - d1 << endl;

	d2.Print();
	cout << endl;

	cout << d2 << endl;

	Date d0(2024, 6, 31);
	d0.Print();

	int num = d4 - d1;
	cout << num << endl;

	int rets = d3 - d2;
	cout << rets << endl;

	d2 += -100;
	d2.Print();

	/*d1.Print();
	d2.Print();
	d3.Print();*/

	/*d1.Print();
	d3 = d2 - 100;
	d3.Print();
	d2 -= 100000;
	d2.Print();*/

	d2 -= -100;
	d2.Print();

	Date ret=d2++;
	ret.Print();

	++d2;
	d2.Print();


	

	return 0;
}

10. 取地址运算符重载

10.1 const成员函数

• 将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后⾯。

• const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进⾏修改。const 修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 const Date* const this(缩小this的权限,使const修饰成员能传进函数)

有些函数如构造函数,要修改成员变量,函数后面就不能加上const

#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}

 // void Print(const Date* const this) const
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
// 这⾥⾮const对象也可以调⽤const成员函数是⼀种权限的缩⼩
Date d1(2024, 7, 5);
d1.Print();

const Date d2(2024, 8, 5);
d2.Print();
return 0;
}

10.2 取地址运算符重载

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器⾃动⽣成的就可以够我们⽤了,不需要去显⽰实现。除⾮⼀些很特殊的场景,⽐如我们不想让别⼈取到当前类对象的地址,就可以⾃⼰实现⼀份,胡乱返回⼀个地址。

11. 再探构造函数(只是完成初始化,不是开空间)

• 之前我们实现构造函数时,初始化成员变量主要使⽤函数体内赋值,构造函数初始化还有⼀种⽅式,就是初始化列表,初始化列表的使⽤⽅式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式

• 每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地⽅。

• 引⽤成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进⾏初始化,否则会编译报错。

• C++11⽀持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显⽰在初始化列表初始化的成员使⽤的。

尽量使⽤初始化列表初始化,因为那些你不在初始化列表初始化的成员也会⾛初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会⽤这个缺省值初始化。如果你没有给缺省值,对于没有显⽰在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有显⽰在初始化列表初始化的⾃定义类型成员会调⽤这个成员类型的默认构造函数,如果没有默认构造会编译错误。

• 初始化列表中按照成员变量在类中声明顺序进⾏初始化跟成员在初始化列表出现的的先后顺序⽆关。建议声明顺序和初始化列表顺序保持⼀致。

初始化列表总结:

⽆论是否显⽰写初始化列表,每个构造函数都有初始化列表;

⽆论是否在初始化列表显⽰初始化,每个成员变量都要⾛初始化列表初始化;

对于const int _n;

int& _ref;

这类const 和 引用成员变量,只能在定义时就进行初始化,所以只能在初始化列表里面进行。

所有成员都要走初始化列表

1、在初始化列表初始化的成员(显示初始化)

2、没有在初始化列表初始化的成员

      a.声明的地方有缺省值用缺省值

      b.没有缺省值

             x:内置类型,不确定,看编译器,大概率时随机值

             y:自定义类型,调用默认构造函数,没有默认构造就编译报错

3、引用const 没有默认构造自定义,必须在初始化列表初始化。

4、按照声明的顺序来进行初始化,与初始化列表出现顺序无关。

12. 类型转换

• C++⽀持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。

• 构造函数前⾯加explicit就不再⽀持隐式类型转换。

• 类类型的对象之间也可以隐式转换,需要相应的构造函数⽀持。

13. static成员(记住在类外进行初始化的格式,int A::_aa)

• ⽤static修饰的成员变量,称之为静态成员变量静态成员变量⼀定要在类外进⾏初始化

静态成员变量为所有类对象所共享不属于某个具体的对象不存在对象中,存放在静态区

• ⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。

• 静态成员函数中可以访问其他的静态成员,但是不能访问⾮静态的,因为没有this指针

⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数。

突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。(在类外可以访问,但不能是私有的)

• 静态成员也是类的成员,受public、protected、private 访问限定符的限制。

• 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不⾛构造函数初始化列表

class Date
{
public:
	Date()
	{
		++_scount;
	}
	~Date()
	{
		--_scount;
	}

	static int GetACount()
	{
		return _scount;
	}
private:
	static int _scount; //在类里面声明
};

int Date::_scount = 0;//只能在类外面初始化

int main()
{
	return 0;
}

14. 友元

• 友元提供了⼀种突破类访问限定符封装的⽅式,友元分为:友元函数和友元类,在函数声明或者类声明的前⾯加friend,并且把友元声明放到⼀个类的⾥⾯。

• 外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数。

• 友元函数可以在类定义的任何地⽅声明,不受类访问限定符限制。

• ⼀个函数可以是多个类的友元函数。

• 友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。

• 友元类的关系是单向的,不具有交换性,⽐如A类是B类的友元,但是B类不是A类的友元。

• 友元类关系不能传递,如果A是B的友元, B是C的友元,但是A不是C的友元。

• 有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多⽤。

#include<iostream>
using namespace std;
// 前置声明,否则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 B;

class A
{
	friend void fuc1(const A& aa, const B& bb); //fuc1()这个函数属于全局
public:
	A()
		:_a1(1)
		,_a2(2)
	{}

	void fuc2(const B& bb);


private:
	int _a1;
	int _a2;
};

class B
{
	friend void fuc1(const A& aa, const B& bb); //fuc1()这个函数属于全局
	friend void A::fuc2(const B& bb); //fuc2()这个函数属于类A::中,是A类的成员变量进行调用的,eg:aa.fuc2(bb);

public:
	B()
		:_b1(3)
		,_b2(4)
	{}

private:
	int _b1;
	int _b2;
};

void fuc1(const A& aa, const B& bb)
{
	cout << aa._a1 << "-" << bb._b1 << endl;
}

void A::fuc2(const B& bb)
{
	cout << this->_a2 << "-" << bb._b2 << endl; //这里也可以不用this,因为此时已经相当于在A类中,直接调用成员变量
}

int main()
{
	A aa;
	B bb;

	fuc1(aa, bb);
	aa.fuc2(bb);

	return 0;
}

友元类:要大量访问别的类的成员,单向性。

class A
{
// 友元声明
friend class B; 友元类,任意访问B类的成员
private:
int _a1 = 1;
int _a2 = 2;
}

此时B是A的友元,但B不是A的友元。

15. 内部类(了解~)

• 如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独⽴的类跟定义在全局相⽐,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。

内部类默认是外部类的友元类。(B可以直接访问A)

• 内部类本质也是⼀种封装,当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; //OK
    cout << a._h << endl; //OK
  }
 };
};


//指定类域 A::B b;

16. 匿名对象

• ⽤ 类型(实参) 定义出来的对象叫做匿名对象,相⽐之前我们定义的 类型 对象名(实参) 定义出来的叫有名对象.

• 匿名对象⽣命周期只在当前⼀⾏,⼀般临时定义⼀个对象当前⽤⼀下即可,就可以定义匿名对象。

int main()
{
A aa1;
// 不能这么定义对象,因为编译器⽆法识别下⾯是⼀个函数声明,还是对象定义
//A aa1();
// 但是我们可以这么定义匿名对象,匿名对象的特点不⽤取名字,
// 但是他的⽣命周期只有这⼀⾏,我们可以看到下⼀⾏他就会⾃动调⽤析构函数

A();
A(1);

A aa2(2);

Solution st;
cout<<st.Sum_Solution(10);
// 匿名对象在这样场景下就很好⽤,当然还有⼀些其他使⽤场景,这个我们以后遇到了再说
Solution().Sum_Solution(10);  //Solution()就相当于st 他的生命周期就只在这一行,相当于一次性
return 0;
}

17. 对象拷⻉时的编译器优化

• 现代编译器会为了尽可能提⾼程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传返回值的过程中可以省略的拷⻉。

• 如何优化C++标准并没有严格规定,各个编译器会根据情况⾃⾏处理。当前主流的相对新⼀点的编译器对于连续⼀个表达式步骤中的连续拷⻉会进⾏合并优化,有些更新更"激进"的编译器还会进⾏跨⾏跨表达式的合并优化。

编写不易~若有不足还请指出!!!>_<

  • 30
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值