2.C++类与对象初步探索

本文详细讲解了面向对象编程中的封装概念,展示了C++中类的定义、访问控制、构造函数、析构函数、运算符重载等内容,并讨论了友元和内部类的特点。通过实例演示了如何在实践中应用这些概念,如日期类的实现和日期到天数的转换。

文章目录

1.面向过程&面向对象

C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
对象之间的交互关系

举例:
外卖系统:🐱
面向过程:下单,接单,送餐的3个过程:🐕
面向对象:客户,商家,骑手,系统🐴(关注这3个类之间的关系)

2.类的引入

C语言中,结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。

//面向过程时,数据和方法(函数)是分离的,重点关注过程-->函数方法
typedef int STDataType;

// c语言
// 数据
struct Stack
{
    STDataType* a;
    int size;
    int capacity;
};
// 函数方法 -- 数据和方法是分离的,重点关注的是过程-》函数方法
void StackPush(struct Stack* ps, STDataType x);
// C++
// 类  -定义出一个新的类型
// 类由两部分构成:1、成员变量 (属性) 2、成员函数 (做的行为)
// C++中的struct兼容C的所有用法,同时C++中把struct升级成类
struct Stack
{
    void Init(int initSize = 4)
    {
        a = (STDataType*)malloc(sizeof(STDataType) * initSize);
        size = 0;
        capacity = initSize;
    }
    void Push(STDataType x)
    {
        // ...
        a[size] = x;
        size++;
    }
    // ...
    STDataType* a;
    int size;
    int capacity;
};
int main()
{
    //struct Stack st;  st被称为对象 这样也行,c++兼容c
    Stack st; //C++可以直接写Stack 就是一种类型  此处才是定义 类里面的成员变量都是声明,还没开辟空间
    st.Init();
    st.Push(1);
    st.Push(2);
    st.Push(3);
    return 0;
}

上面结构体的定义,在C++中更喜欢用class来代替

3.类的定义

class className
{
 // 类体:由成员函数和成员变量组成
 
}; // 一定要注意后面的分号

class为定义类的关键字ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号
类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数

声明定义分离时,定义时要加上类的域不然找不到

类的两种定义方法

1.声明和定义全部放在类体中

声明和定义全部放在类体中,需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理

class Student
{
	void SetStudentInfo(const char* name, const char* gender, int age)
	{
		strcpy(_name, name);
		strcpy(_gender, gender);
		_age = age;
	}

	void PrintStudentInfo()
	{
		cout << _name << " " << _gender << " " << _age << endl;
	}

	char _name[20];
	char _gender[3];
	int _age; //成员变量在这里是声明,还没有给他开辟空间
};

2.声明定义分离

//声明放在.h文件中,类的定义放在.cpp文件中
//person.h
class Person
{
public:
   //显示信息
   void show();
public:
   char* _name;
   char* _sex;
   int _age;
}

//person.cpp
#include"person.h>

void Person::show()
{
   cout<<_name<<"  "<<_sex<<"  "<<_age<<endl;
}   

注意:一般情况下我们采用第二种方式

4.类的访问限定符及封装

访问限定符

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

【访问限定符说明】

  1. public修饰的成员在类外可以直接被访问
  2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
  3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
  4. class的默认访问权限为private,struct为public(因为struct要兼容C)

面向对象特性

三大特性:封装,继承,多态
实际还有抽象,反射

封装

封装本质上是一种管理:我们使用类将数据和方法都封装起来。不想对外开放的就用 protected/private 封装起来,用 public 封装的成员允许外界对其进行合理的访问。

5.类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域。

6.类的实例化

用类类型创建对象的过程,称为类的实例化

  1. 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
  2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
  3. 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间
class Person
{
public:
    void PrintPersonInfo();
private://成员变量的命名风格区分一下
    char _name[20];
    char _gender[3];
    int _age;
};

void test()
{
    Person man;   //类的实例化
    man.PrintPersonInfo();
}

7.类对象模型

类对象的大小

class Stack
{
public:
    void Init(int initSize = 4);
	void Push(STDataType x);

	// ...
private:
	STDataType* a;
	int size;
	int capacity;
};
void Stack::Init(int initSize)
	{
		a = (STDataType*)malloc(sizeof(STDataType) * initSize);
		size = 0;
		capacity = initSize;
	}

void Stack::Push(STDataType x)
	{
		// ...
		a[size] = x;
		size++;
	}
int main()
{
	//struct Stack st;
	Stack st;
	st.Init();
	st.Push(1);
	st.Push(2);
	st.Push(3);

    Stack st2;
    cout << sizeof(st) << endl;//16 
    cout << sizeof(st2) << endl;//16 64位环境下 结构体总大小为最大对齐数的整数倍
	return 0;
}

类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小? 想要知道这个,首先我们要弄明白类在内存中的存储方式。

类对象的存储方式

image-20220221201549924

那为什么内存要这样存储类了?
原因:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。

结论:一个类的大小,实际就是该类中”成员变量”之和,注意要进行内存对齐

空类

//空类
class A2
{};
//类中仅有成员函数
class A3
{
public:
    void f2()
    {

    }

};
void test2()
{
    A2 a2;
    A3 a3;
    cout << sizeof(a2) << endl;//1
    cout << sizeof(a3) << endl;//1
}
int main()
{
	test2();
	return 0;
}

注意空类的大小是1,空类比较特殊,编译器给了空类一个字节来唯一标识这个类,是为了占位,表示对象存在过,不同对象地址不同

8.this 指针

赋初值

这样赋初值不起作用,因为遵循就近原则,year自己赋值给自己
要么就用类域去限制,要么就区分成员变量的命名风格

image-20220221203852812

this指针的引出

为什么不同对象调用的同一个函数,最后却都能初始化到自己身上?
函数体中没有关于不同对象的区分,那当d1调用Init函数时,该函数是如何知道应该初始化d1对象,而不是初始化d2对象呢?

C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成

image-20220221205102958

image-20220221205953410

class Date
{
private:
	//凡是成员变量,建议区分命名风格
	int _year;
	int _month;
	int _day;

public:
	//编译器会增加一个隐含的参数void Init(Date* this, int year,int month, int day)
	// 1.this指针是隐含的,我们不能显性的在调用和函数定义中加
	// 2.但我们可以在成员函数中使用this指针
    // 3.this是形参,一般是存在栈上的,不同的编译器不同,vs是使用ecx寄存器存储,传参

	void Init(int year, int month, int day)
	{
		//检查日期合法性
		//...
		//不写this,编译器也会自动加上
		// _year = year;
		// _month = month;
		// _day = day;
		//编译器会处理成:
		this->_year = year;
		this->_month = month;
		this->_day = day;
		cout << "this:" << this << endl;
	}
};

void test3()
{
	Date d1;
	d1.Init(2021, 2, 21); //编译器会处理成 d1.Init(&d1, 2021, 2, 21)
	cout << "d1:" << &d1 << endl;
	Date d2;
	d2.Init(2021, 2, 22); //编译器会处理成 d2.Init(&d2, 2021, 2, 22)
	cout << "d2:" << &d2 << endl;

}

image-20220221210922288

image-20220302103006609

this指针的特性

  1. this指针的类型:类类型* const
  2. 只能在“成员函数”的内部使用
  3. this指针本质上其实是一个成员函数的形参,存在栈里面的,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针
  4. this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
  5. this是一个关键字

练习

注意:this指针不能为空下面来看一个练习

1.下面程序能编译通过吗?
2.下面程序会崩溃吗?在哪里崩溃
class A
{
public:
	void PrintA()
	{
		cout << _a << endl;
	}

	void Show()
	{
		cout << "Show()" << endl;
	}
private:
	int _a;
};

//可以编译通过
int main()
{
	A* p = nullptr;
	p->PrintA(); // 空指针解引用崩溃
	p->Show();     // 正常运行,没有涉及到成员变量
}

image-20220221212615242

对象里面只存有成员变量

p->Show(); 调用时,this指针虽然是空,但并没有去访问对象里的成员变量,就不会产生空指针解引用的问题

9.类的6个默认成员函数

如果一个类中什么成员都没有,我们简称其为空类。但是空类中真的什么都没有吗?其实不然,任何一个类,即使我们什么都不写,类中也会自动生成6个默认成员函数。

生成的不一定好用,最好还是自己写

class Date {}; //空类

image-20220221213228538

1.构造函数

//1.构造函数 --> 完成对象初始化 顶替Init()
//我们有时可能会忘记初始化对象,就去使用它,C++为了解决这个缺陷,引入了构造函数

class Date2
{
private:
	int _year;
	int _month;
	int _day;
public:
	//构造函数 对象实例化的时候自动调用,保证对象的初始化
	//一般情况,对象初始化分为默认值初始化和给定值初始化
	//利用全缺省参数,将2者合二为一
	Date2(int year = 2020, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// Date2(int year, int month, int day)
	// {
	// 	_year = year;
	// 	_month = month;
	// 	_day = day;
	// }
	// //Date2重载
	// Date2()
	// {
	// 	_year = 2000;
	// 	_month = 1;
	// 	_day = 1;
	// }
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
};

void test4()
{
	Date2 d1(2022,2,21);
	d1.Print();//2022-2-21

	Date2 d2(2022);
	d2.Print();//2022-1-1

	Date2 d3;//无参的不能再加(),不然就变成函数声明了
	d3.Print();//2000-1-1
}

注意:构造函数的主要任务并不是开空间创建对象,而是初始化对象。

全缺省和无参的构造函数语法上虽然能产生重载,但会产生二义性

构造函数特性

  1. 函数名与类名相同。
  2. 无返回值。注意void 表示的是返回值为空
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载。可以有多种初始化方式

默认构造函数初始化

如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
所以说构造函数叫默认成员函数

class Date
{
public:
private:
    int _year;
    int _month;
    int _day;
};

void Test()
{
    // 没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数
    Date d; 
}

编译器自动生成的默认构造函数对待内置类型和自定义类型会区别对待,编译器会去调用自定义类型的默认构造函数来初始化

注意:如果A类中没有自己写构造函数,用编译器默认的构造函数,它也是一样会输入随机值的。

image-20220221224056726

class A
{
public:
	A(int a = 0)
	{
		cout << "A(int a = 0)构造函数" << endl;
		_a = a;
	}
	void Print()
	{
		cout << _a << endl;
	}
private:
	int _a;
};

class Date4
{
public:
	// 我们不写,编译器会生成一个构造函数,我们写了编译器就不会生成了。所以说构造函数叫默认成员函数
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
		_aa.Print();
	}

private:
	int _year;
	int _month;
	int _day;

	A _aa;
};

int main()
{
	Date4 d2;//Date4类中用的是编译器自动生成的构造函数,因为都是内置类型,因此是随机值
	d2.Print();

	return 0;
}
class A
{
public:
	A(int a)//不是默认构造函数
	{
		cout << "A(int a = 0)构造函数" << endl;
		_a = a;
	}
	void Print()
	{
		cout << _a << endl;
	}
private:
	int _a;
};
//如果改成这样,手动创建了构造函数,但又不是默认构造函数,编译器本该去调用自定义类型的默认构造函数,但找不到,因此编译通不过

默认构造函数

不用参数就能调用的那个函数

  1. 无参构造函数
  2. 全缺省构造函数
  3. 我们没写编译器默认生成的构造函数

注意:默认构造函数只能有一个,其实也就是不用传参就能调用的构造函数

// 默认构造函数
class Date
{ 
public:
    Date()
    {
        _year = 1900 ;
        _month = 1 ;
        _day = 1;
    }

    Date (int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
private :
    int _year ;
    int _month ;
    int _day ;
};
// 以下测试函数能通过编译吗?
void Test()
{
    Date d1; 
}

总结:大多数情况下都要自己写构造函数完成初始化,最好写一个全缺省的构造函数,这样能适应各种场景

2.析构函数

析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。
而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。析构函数就是针对的就是C里面内存泄漏的缺陷,我们时常会忘记手动回收

Date类的析构函数没有意义,因为局部变量系统会自动销毁的,不需要析构函数处理,Stack类的析构函数就有意义
如果系统自动调用的析构函数也啥不做的话,release版本下就会被优化掉

class Stack2
{
private:
	int *_a;
	int _size;
	int _capacity;

public:
	Stack2(int capacity = 4);
	~Stack2();
	void Push(int x)
	{

	}
};

Stack2::Stack2(int capacity)
{
	_a = (int *)malloc(sizeof(int) * capacity);
	_size = 0;
	_capacity = capacity;
}

Stack2::~Stack2()
{
	free(_a);
	_a = nullptr;
	_size = _capacity = 0;
}
int main(int argc, char const *argv[])
{
	Stack2 st1;
	st1.Push(1);
	st1.Push(2);
	st1.Push(3);
	return 0;
}//运行到这里,也就是对象的生命周期结束之后才自动调用析构函数的
//调试时只能看到当前作用域的变量,因此可以借助this指针观察

特性

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值。不能重载
  3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
    编译器自动生成的析构函数机制:
     1、编译器自动生成的析构函数对内置类型不做处理。
     2、对于自定义类型,编译器会再去调用它们自己的默认析构函数。
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
  5. 先构造的后析构,后构造的先析构

调用顺序

对象是定义在函数中的,函数调用会建立栈帧,栈帧中的对象构造和析构也要符合后进先出

int main(int argc, char const *argv[])
{
	Stack2 st1;//st1先构造,先入栈
	st1.Push(1);
	st1.Push(2);
	st1.Push(3);

	Stack2 st2;//st2后构造,后入栈
	st2.Push(1);
	st2.Push(2);
	st2.Push(3);
	return 0;
}//st2在上面,st2先析构,st1后析构

image-20220221233525185

栈&堆

数据结构中有栈与堆,内存分段区域中也有栈与堆,他们有什么联系和区别?

  1. 他们之间没有绝对的联系,属于2个学科各自的命名,一个是数据结构,一个是分段(一段内存的命名)
  2. 数据结构和系统分段的栈(函数栈帧)中的对象都符合后进先出原则
  3. 堆则完全没有联系了

默认析构函数

编译器默认生成的析构函数

  1. 针对内置类型成员:不做处理
  2. 针对自定义类型成员:会去调用它的析构函数
class Stack2
{
private:
	int *_a;
	int _size;
	int _capacity;

public:
	Stack2(int capacity = 4);
	~Stack2();
	void Push(int x)
	{

	}
};

Stack2::Stack2(int capacity)
{
	_a = (int *)malloc(sizeof(int) * capacity);
	_size = 0;
	_capacity = capacity;
}

Stack2::~Stack2()
{
	cout << "Stack2::~Stack2() 析构函数" << endl;//方便观察
	free(_a);
	_a = nullptr;
	_size = _capacity = 0;
}

class Date5
{
public:
private:
	int _year;
	int _month;
	int _day;

	Stack2 _st;
};

int main(int argc, char const *argv[])
{
	Date5 d;//Stack2::~Stack2() 析构函数
	return 0;
}

3.拷贝构造函数

特殊的构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用

默认成员函数

class Date7
{
public:
	Date7(int year = 2000, 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;
};

int main(int argc, char const *argv[])
{
	Date7 d1(2022,2,21);
	Date7 d2;
	//Date7 d3();  不能这样写,这样变成了函数声明,并没有去构造对象

	//如果想复制d1的值
	Date7 d4(d1);//拷贝构造

	d1.Print();//2022-2-21
	d2.Print();//2000-1-1
	d4.Print();//2022-2-21
	return 0;
}

我们没写拷贝构造函数,编译器就自己生成一个,完成拷贝任务

编译器自动生成的拷贝构造函数对内置类型会完成浅拷贝(值拷贝)

针对自定义类型会去调用它的拷贝构造函数完成拷贝

class A4
{
public:
	A4(int a = 1)
	{
		cout << "A(int a = 1)构造函数" << endl;
		_a = a;
	}
	A4(const A4& a)
	{
		cout << "A4(const A4& a)" << endl;
		_a = a._a;
	}
	~A4()
	{
		cout << "~A4() 析构函数" << endl;
	}

private:
	int _a;
};
class Date7
{
public:
	// 我们不写,编译器会生成一个构造函数,我们写了编译器就不会生成了。所以说构造函数叫默认成员函数
	Date7(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
    
	//Date7 d4(d1);  拷贝构造函数
	// Date7(Date7& d)//d4 传给this d1传给d
	// {
	// 	_year = d._year;
	// 	_month = d._month;
	// 	_day = d._day;
	// }
 
private:
	int _year;
	int _month;
	int _day;
	
	A4 _aa;
};
int main(int argc, char const *argv[])
{
	 Date7 d1(2022,2,21);
	Date7 d4(d1);//拷贝构造
	d1.Print();
	d4.Print();
	system("pause");
	return 0;
}
输出结果:
A(int a = 1)构造函数
A4(const A4& a) 拷贝构造
2022-2-21
2022-2-21
请按任意键继续. . .
~A4() 析构函数
~A4() 析构函数

特性

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。

要调用拷贝构造,就要先传参,传参使用传值调用,又是对象拷贝构造,陷入了无穷递归调用

image-20220222104007520

image-20220222104228758

如果传引用调用,那么d1是d的别名,d4赋值给 this 指针,就解决了

注意:传指针虽然也能达到类似效果,但调用时就需要&地址,不方便

Date7(const Date7& d)//d4地址传给this d1传给d  (d是d1的别名)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
//建议加上const 缩小权限,不能修改d的值,防止函数里面写迷糊,写成了d._year = _year;

image-20220222105709908

传值传参还要去调用拷贝构造,效率不如引用传参

总结:

函数传参,自定义类型的对象,一般推荐引用传参,如果继续使用传值传参,每次都要调用拷贝构造

void f1(Date7 d)
{

}

void f2(Date7& d)
{
	
}
void test()
{
    Date7 d1(2022,2,21);
	f1(d1);
	f2(d1);
}

浅拷贝问题

若未显示定义拷贝构造函数,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

一般涉及到堆区的问题,浅拷贝是无法解决问题的。

class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int)* capacity);
		_size = 0;
		_capacity = capacity;
	}
	void Print()
	{
		cout << _ps << endl;// 打印栈空间地址
	}
private:
	int* _a;
	int _size;
	int _capacity;
};

int main()
{
	Stack st;
	st.Print();// 打印st栈空间的地址
	Stack copy(st);// 用已存在的对象st创建对象copy
	copy.Print();// 打印copy栈空间的地址
	return 0;
}
//程序会崩溃

image-20220222112255408

copy后创建,会先析构,析构时虽然先 free(_a);_a = nullptr;但操作的只是copy的 _a,并没有影响到 st 的_a

而且调用st的析构函数时,又会free(_a);,一块空间被free了2次

而且st 和 copy指向的是同一片空间,其中一个对象插入删除数据,会导致另一个对象也会被影响

总结

  1. Date这样的类,需要的是浅拷贝,默认生成的拷贝构造就可以用了,不需要自己写
  2. Stack这样的类,需要的是深拷贝,浅拷贝会导致析构2次,程序崩溃等问题,需要自己写

运算符重载

问题引入

比较2个日期是否相等

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

	//d1.Equal(d2)  d1传给了this d2传给了d
	bool Equal(Date6 d) //隐含了一个this指针,因此只需要一个参数即可比较
	//如果写成全局函数,那就需要2个参数了,但又不能访问私有成员变量
	{
		return _year == d._day && _month == d._month && _day == d._day;
	}

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

int main(int argc, char const *argv[])
{
	Date6 d1(2022, 2, 22);
	Date6 d2(2022, 2, 21);
	cout << d1.Equal(d2) << endl;//0
	return 0;
}

但这样些可读性差,万一别人胡写,写成了Fun1,Fun2这种就。。。
如果函数名写成 d1 == d2 d1 > d2 d1 < d2 这样就好了

解决办法

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

内置类型:语言层面就支持运算符重载

自定义类型:默认不支持,但是C++的运算符重载机制可以让类对象支持用某个运算符

函数原型:返回值类型 operator操作符(参数列表)

注意

  1. 不能通过连接其他符号来创建新的操作符:比如operator@
  2. 重载操作符必须有一个类类型或者枚举类型的操作数
  3. 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义
  4. 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的
  5. 操作符有一个默认的形参this,限定为第一个形参
  6. .*、::、sizeof、?:、 . 注意以上5个运算符不能重载。
    注意是 .* 连在一起的

运算符重载&函数重载

  1. 虽然都有重载,但二者没有联系
  2. 函数重载支持定义同名函数
  3. 运算符重载是为了让自定义类型可以像内置类型一样去使用运算符

operator==

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

	bool operator==(Date6 d) //隐含了一个this指针,因此只需要一个参数即可比较
	//如果写成全局函数,那就需要2个参数了,但又不能访问私有成员变量
	{
		return _year == d._day && _month == d._month && _day == d._day;
	}

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

int main(int argc, char const *argv[])
{
	Date6 d1(2022, 2, 22);
	Date6 d2(2022, 2, 21);
	cout << d1.operator==(d2) << endl;//0
	//编译器会把 d1.operator==(d2) 转换成 d1.operator==(&d1, d2)
	int tmp = d1==d2;
	//编译器会把 d1==d2 转换成 d1.operator==(&d1, d2)
	cout << tmp << endl;//0
	//<< 优先级比较高 cout << (d1==d2) << endl; 这样也行
    return 0;
}

operator<

bool operator<(const Date6& d)
    //转换成 bool operator<(const Date6* this d1, const Date6& d)
    //不希望被修改就加上const
	{
		if (_year < d._year)
		{
			return true;
		}
		else if (_year == d._year)
		{
			if(_month < d._month)
			{
				return true;
			}
			else if(_month == d._month)
			{
				if(_day < d._day)
				{
					return true;
				}
			}
		}
		return false;
	}

operator[ ]

class Array
{
private:
	int _a[10];

public:
	Array()
	{
		for (size_t i = 0; i < 10; i++)
		{
			_a[i] = i * 10;
		}
	}
	int& operator[](size_t pos)
	{
		return _a[pos];//返回的是别名(引用)
	}
};

void test()
{
    Array ay;
	cout << ay[0] << endl;//转换成 ay.operator[](&ay, 0);
	cout << ay[1] << endl;
	cout << ay[2] << endl;
	cout << ay[3] << endl;

	//修改 -- 赋值给了返回对象的别名
	ay[0] = 100;
	ay[1] = 200;
	ay[2] = 300;
	ay[3] = 400;

	cout << ay[0] << endl;//转换成 ay.operator[](&ay, 0);
	cout << ay[1] << endl;
	cout << ay[2] << endl;
	cout << ay[3] << endl;
}

4.赋值运算符重载

class Date7
{
public:
	Date7(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

	//d1 = d2 传引用就无需再去调用拷贝构造
    //di地址传给this d2传给d
	void operator=(const Date7& d) // void operator=(Date7* this, const Date7& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
 
private:
	int _year;
	int _month;
	int _day;
	
	A4 _aa;
};
int main(int argc, char const *argv[])
{
	Date7 d1(2022,2,21);
	Date7 d2;

	//这样也是拷贝行为,但不一样的是,拷贝构造是创建一个对象时,拿同类对象初始化的拷贝
	//这里的复制拷贝时,两个对象已经存在,都被初始化过了,现在是想把一个对象,复制拷贝给另一个对象
	d1 = d2;
	d1.Print();//2000-1-1
	d2.Print();//2000-1-1
	system("pause");
	return 0;
}

连续赋值

i = j = k j = k 表达式返回值是 j

d1 = d2 = d3 要返回 d2 才行,返回值也是一个Date7类,d2地址是传给this的

//d1 = d2 = d3 传值传引用就无需再去调用拷贝构造
Date7 operator=(const Date7& d) // Date7 operator=(Date7* this, const Date7& d)
{
    _year = d._year;
    _month = d._month;
    _day = d._day;
    return *this;
}

但这也返回的是值,又回去调用拷贝构造,此处 的*this (*this)就是d2出了函数作用域也还在,可以传引用返回,避免了调用拷贝构造

//d1 = d2 传值传引用就无需再去调用拷贝构造
Date7& operator=(const Date7& d) // void operator=(Date7* this, const Date7& d)
{
    _year = d._year;
    _month = d._month;
    _day = d._day;
    return *this;
}//返回的是*this的别名

避免d1 = d1 这种自己给自己赋值

//d1 = d2 传值传引用就无需再去调用拷贝构造
Date7& operator=(const Date7& d) // void operator=(Date7* this, const Date7& d)
{
    if(this != &d) //不是自己给自己赋值才需要拷贝
    {
        _year = d._year;
    	_month = d._month;
    	_day = d._day;
    }
    return *this;
}//返回的是*this的别名

默认成员函数

赋值运算符的重载也是一个默认成员函数,我们不写,编译器也会自动生成

默认生成的赋值运算符重载跟拷贝构造特性一致

  1. 针对内置类型,会完成浅拷贝
  2. 针对自定义类型,会调用它的赋值运算符重载完成拷贝
class Date7
{
public:
	Date7(int year = 2000, 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;
};
int main(int argc, char const *argv[])
{
	Date7 d1(2022, 2, 21);
	Date7 d2;
	d2 = d1;
	d1.Print(); // 2022-2-21
	d2.Print(); // 2022-2-21
	system("pause");
	return 0;

区分拷贝和赋值

Date d1(2022, 6, 1);
Date d4;
Date d2(d1);//拷贝构造,d2还不存在
Date d3 = d1;//拷贝构造,d3还不存在
d4 = d3;//赋值重载 d4 d3均已经存在

拷贝构造函数:用一个已经存在的对象去构造初始化另一个即将创建的对象。
赋值运算符重载函数:在两个对象都已经存在的情况下,将一个对象赋值给另一个对象。

特性

  1. 参数类型
  2. 返回值
  3. 检测是否自己给自己赋值
  4. 返回 *this
  5. 默认生成

5and6.operator&

//基本没有实现价值 除非你不想让别人获取Date类对象地址,才需要自己实现
Date* operator&()
{
    return this;
}

const Date* operator&() const
{
    return this;
}

Date d1(2022,2,22);
const Date d2(2022,2,22);
&d1; //调用Date* operator&()
&d2;//调用const Date* operator&() const

这也是默认成员函数之一,不需要写,编译器也能生成

总结:

编译器默认生成的情况下:

构造和析构类似,内置类型不处理,自定义类型调用他们的构造和析构

拷贝构造和赋值重载类似,内置类型完成浅拷贝,自定义类型会调用他们的拷贝构造和赋值重载

练习:📅Date类📅

Date.h

#pragma once
#include <iostream>
#include <assert.h>
//项目中最好不要改命名空间 using namespace std;只需展开常用的就行
using std::cout;
using std::cin;
using std::endl;
class Date
{
private:
    int _year;
    int _month;
    int _day;
public:
    //构造函数
    Date(int year = 0, int month = 1, int day = 1);
    void Print();
    //析构,拷贝构造,赋值重载不需要写,默认生成就够用了
    
    //d += 100
    Date& operator+=(int day);
    Date operator+(int day);
    //d -= 100
    Date& operator-=(int day);
    Date operator-(int day);
    Date& operator++();//前置++
    Date operator++(int);//后置++
    Date& operator--();//前置--
    Date operator--(int);//后置--
    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);
    //日期相减
    int operator-(const Date& d);
};

GetMonthDay

inline int GetMonthDay(int year, int month)
{
    //数组存储平年每个月天数
    //这个函数会被频繁调用,应定义为inline 且数组放在静态区,避免反复开辟
    static int dayArray[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    int day = dayArray[month];
    if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) // 4年一润且百年不润,或者400年润
    {
        //判断顺序交换一下,只有2月才需要判断是否是闰年,提高效率
        day = 29;
    }
    return day;
}

Date 构造函数

//缺省参数一般就写在声明中
Date::Date(int year, int month, int day) //构造函数
{
    //检查日期合法性
    if (year >= 0 && month > 0 && month < 13 && day > 0 && day < GetMonthDay(year, month))
    {
        _year = year;
        _month = month;
        _day = day;
    }
    else
    {
        cout << "非法日期" << endl;
        assert(false);
    }
}

void Date::Print() const
{
    cout << _year << "年" << _month << "月" << _day << "日" << endl;
}

operator+=

// d += 10 会改变d
Date &Date::operator+=(int day)//出了作用域对象还在就可以返回引用
    //不用引用返回也可以,但会增加一次拷贝
{
    if (day < 0)
    {
        *this -= -day;
    }
    else
    {
        _day += day;
        while (_day > GetMonthDay(_year, _month))
        {
            // 1.天满了,减去当月的天数,月+1
            _day -= GetMonthDay(_year, _month);
            _month++;
            if (_month > 12)
            {
                // 2.月满了,年+1,月置成1
                ++_year;
                _month = 1;
            }
        }
    }
    return *this; //*this 就是加完的年月日
    //这其实实现的功能是 += 而不是 +
}

// d1 + 10
Date Date::operator+(int day) const //正好保护d
{
    //创建临时对象
    Date ret(*this); //调用拷贝构造
    //复用operator+=
    ret += day;//因为在+=里面处理了负数的情况,+就不用再额外处理负数的
    return ret; //临时对象不能用引用返回,出了作用域,临时对象就销毁了
}

+=返回的是d本身
可以实现(i+=10) += 10;(d1+=10) += 10;变成了左值(可修改的)

operator-=

// d1 -= 100  d1 -= -100
Date &Date::operator-=(int day)
{
    //考虑day是负数
    if (day < 0)
    {
        //复用
        *this += -day;
    }
    else
    {
        _day -= day;
        while (_day <= 0)
        {
            --_month;
            if (_month == 0)
            {
                --_year;
                _month = 12;
            }
            //加上上个月的天数	借位
            int preMonthDay = GetMonthDay(_year, _month);
            _day += preMonthDay;
        }
    }
    return *this;
}

Date Date::operator-(int day) const
{
    // Date tmp = *this;
    Date tmp(*this);
    tmp -= day;
    return tmp;//出了作用域就被销毁了
}

前置后置++ - -

// ++d & d++
Date &Date::operator++() //前置++ d++ ++d 返回值不同
{
    *this += 1;
    return *this; //前置++是先++
}

Date Date::operator++(int) //后置++  C++之父规定的
{
    // int 参数不需要给实参,仅仅是为了构成重载,从而区分前置++ 和后置++而已
    Date tmp(*this);
    *this += 1;
    return tmp; //后置++ 先使用d再++ 返回的是++之前的值
}

Date &Date::operator--() //前置--
{
    *this -= 1;
    return *this;
}

Date Date::operator--(int) //后置--
{
    Date tmp(*this);
    *this -= 1;
    return tmp;
}

比较

/只需要先实现一个> == 剩下的都可以去复用
bool Date::operator==(const Date &d) const
{
    if (_year == d._year && _month == d._month && _day == d._day)
    {
        return true;
    }
    return false;
}
bool Date::operator<(const Date &d) const
{
    if ((_year < d._year) ||
        (_year == d._year && _month < d._month) ||
        (_year == d._year && _month == d._month && _day < d._day))
        return true;
    return false;
}

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

operator-

//d1 - d2  日期+日期没有意义
int Date::operator-(const Date &d) const
{
    //思路:让小的不断++,直到==max
    Date max = *this;
    Date min = d;
    int flag = 1;
    if (*this < d)
    {
        //判断错误
        max = d;
        min = *this;
        flag = -1;
    }
    int count = 0;
    while (min != max)
    {
        ++min;
        ++count;
    }
    return count*flag;
}

10.const修饰成员函数

const修饰类成员函数,实际修饰的是类成员函数隐含的this指针,表明在该成员函数中不能对this指针指向的对象进行修改。

bool operator==(const Date &d) const
    //==> bool operator==(const Date* this, const Date &d)
    //确保this指向的内容不被修改,在括号后面写上const
    //如果函数中不小心修改了成员变量,编译时就能检查出来
    {
        if (_year == d._year && _month == d._month && _day == d._day)
        {
            return true;
        }
        return false;
    }

注意:在使用const时要注意,权限不能放大,但是可以缩小

image-20220222223017457

如果Print不加const
const Date d3(2021,5,27);反而无法调动Print函数,因为d3是只读,而不加const的Print是可读可写的

思考

  1. const对象可以调用非const成员函数吗?❌

    const 对象是只读的,如果传给了非const的成员函数就变成可读可写,权限放大,所以不能调用

  2. 非const对象可以调用const成员函数吗?🉑

    权限缩小,可以

  3. 非const成员函数内可以调用其他的const成员函数吗?🉑

    权限缩小,可以

    void Print() const
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
    //成员函数中,编译器处理以后在成员(成员变量/成员函数)前面加上this->
    void Fun1()
    {
        Print();//this->Print();
    }
    
  4. const成员函数内可以调用其他的非const成员函数吗?❌

    this指针是只读的,传过去变成可读可写,权限放大,不能调用

    image-20220303100032246

operator&

//基本没有实现价值
Date* operator&()
{
    return this;
}

const Date* operator&() const
{
    return this;
}

Date d1(2022,2,22);
const Date d2(2022,2,22);
&d1; //调用Date* operator&()
&d2;//调用const Date* operator&() const

这也是默认成员函数之一,不需要写,编译器也能生成

11.再谈构造函数

构造函数体赋值

在创建对象时,编译器会通过调用构造函数,给对象中的各个成员变量一个合适的初始值:

class Date
{
public:
	// 构造函数
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化,构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值

class Date
{
public:
	// 构造函数
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;// 第一次赋值
		_year = 2022;// 第二次赋值
		//...
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

初始化列表

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。

class Date
{
public:
	// 构造函数  
	Date(int year = 0, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

构造函数体内赋值和初始化列表可以混着用的

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

注意事项

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

  2. 类中包含以下成员,必须放在初始化列表进行初始化

    1. 引用成员变量
      引用类型的变量在定义时就必须给其一个初始值,所以引用成员变量必须使用初始化列表对其进行初始化。

      int a = 10;
      int& b = a;// 创建时就初始化
      
    1. const成员变量
      被const修饰的变量也必须在定义时就给其一个初始值,也必须使用初始化列表进行初始化

      const int a = 10;//correct 创建时就初始化
      const int b;//error 创建时未初始化
      
  1. 自定义类型成员(该类没有默认构造函数)
    若一个类没有默认构造函数,那么我们在实例化该类对象时就需要传参对其进行初始化,所以实例化没有默认构造函数的类对象时必须使用初始化列表对其进行初始化。
class A
{
public:
    A(int x = 0)//有默认构造函数
    {
        cout << "A(int x = 0)" << endl;
        _x = x;
    }

private:
    int _x;
};

class Date
{
public:
    //一个对象的单个成员变量,在初始化列表是它的定义的阶段
    Date(int year = 0, int month = 1, int day = 1)
        : _year(year), _month(month) //这里的_year _month _day 定义时可以不初始化
          ,
          _n(10), _ref(year)
    {
        _day = day;
    }
private:
    //这里仅仅是成员变量的声明
    int _year;
    int _month;
    int _day;

    //它们必须在定义的时候初始化
    const int _n;
    int &_ref;
    A _a; //自定义类型,会自动调用它的默认构造函数,因此初始化列表里不写也行
};
class A
{
public:
    A(int x)
    {
        cout << "A(int x = 0)" << endl;
        _x = x;
    }

private:
    int _x;
};

class Date
{
public:
    //一个对象的单个成员变量,在初始化列表是它的定义的阶段
    Date(int year = 0, int month = 1, int day = 1)
        : _year(year), _month(month) //这里的_year _month _day 定义时可以不初始化
          ,
          _n(10), _ref(year), _a(1)//显示去调用
    {
        _day = day;
    }

private:
    //这里仅仅是成员变量的声明
    int _year;
    int _month;
    int _day;

    //它们必须在定义的时候初始化
    const int _n;
    int &_ref;
    A _a; //自定义类型,会自动调用它的默认构造函数,因此初始化列表里不写也行
};

在这里再声明一下,默认构造函数是指不用传参就可以调用的构造函数:
 1.我们不写,编译器自动生成的构造函数。
 2.无参的构造函数。
 3.全缺省的构造函数。

  1. 尽量使用初始化列表初始化

因为初始化列表实际上就是当你实例化一个对象时,该对象的成员变量定义的地方,所以无论是否使用初始化列表,都会走这么一个过程(成员变量需要定义出来)。

严格来说:
1.对于内置类型,使用初始化列表和在构造函数体内进行初始化实际上是没有差别的,其差别就类似于如下代码:

// 使用初始化列表
int a = 10
// 在构造函数体内初始化(不使用初始化列表)
int a;
a = 10;

2.对于自定义类型,使用初始化列表可以提高代码的效率

class Time
{
public:
	Time(int hour = 0)
	{
		_hour = hour;
	}
private:
	int _hour;
};
class Test
{
public:
	// 使用初始化列表
	Test(int hour)
		:_t(12)// 调用一次Time类的构造函数
	{}
private:
	Time _t;
};

对于以上代码,当我们要实例化一个Test类的对象时,我们使用了初始化列表,在实例化过程中只调用了一次Time类的构造函数。

//我们若是想在不使用初始化列表的情况下,达到我们想要的效果,就不得不这样写了:
class Time
{
public:
	Time(int hour = 0)
	{
		_hour = hour;
	}
private:
	int _hour;
};
class Test
{
public:
	// 在构造函数体内初始化(不使用初始化列表)
	Test(int hour)
	{
		Time t(hour);// 调用一次Time类的构造函数
		_t = t;// 调用一次Time类的赋值运算符重载函数
	}
private:
	Time _t;
};
//这时如果我们要实例化一个Test类的对象,在实例化过程中先调用了一次Time类的构造函数,又调用了一次Time类的赋值运算符重载函数,效率就降下来了。
  1. 成员变量在类中声明的次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后顺序无关

举个例子来看看:

#include <iostream>
using namespace std;
int i = 0;
class Test
{
public:
	Test()
		:_b(i++)
		,_a(i++)
	{}
	void Print()
	{
		cout << "_a:" << _a << endl;
		cout << "_b:" << _b << endl;
	}
private:
	int _a;
	int _b;
};
int main()
{
	Test test;
	test.Print(); //结果test._a为0,test._b为1
	return 0;
}

代码中,Test类构造函数的初始化列表中成员变量_b先初始化,成员变量_a后初始化,按道理打印结果test._a为1,test._b为0,但是初始化列表的初始化顺序是成员变量在类中声明次序,所以最终test._a为0,test._b为1。

建议:类中成员变量声明的顺序和初始化列表出现的顺序保持一致

单参数构造函数

class A
{
private:
    int _a;

public:
    A(int a) // construction
    : _a(a)
    {
        cout << "A(int a)//construction" << endl;
    }
    A(const A &aa) // copy construction
    {
        cout << " A(const A& aa)//copy construction" << endl;
    }
};
int main(int argc, char const *argv[])
{
    A aa1(1);
    A aa2(aa1); //拷贝构造,编译器自动生成的就够用了,只有内置类型
    // A aa3 = 2;
    const A &aa3 = 2; //单参数的构造函数,支持隐式类型转换
    //语法意义上,先构造再拷贝
    //相当于 早期的编译器--> A tmp(2)  A aa3(tmp)
    //现在的编译器做了优化,直接调用构造函数 -->A aa3(2)
    //打印结果是 A(int a)//construction

    //类似:
    int i = 0;
    const double &d = i; //引用的是临时变量,需要加const
    system("pause");
    return 0;
}

支持单参构造函数的隐式类型转换意义?

string s11("hello");
string s22 = "hello";//string(const char* str){}

vector<string> v;  //--> push_back(const string& s){}
string s1("hello");//如果不支持转换,只能这么写
v.push_back(s1);

v.push_back("hello world");//string支持单参数的隐式类型转换,才能这样写,这样传参也更便捷

匿名对象

class A
{
private:
    int _a;

public:
    A(int a) // construction
    : _a(a)
    {
        cout << "A(int a)//construction" << endl;
    }
    A(const A &aa) // copy construction
    : _a(aa._a)
    {
        cout << " A(const A& aa)//copy construction" << endl;
    }
    ~A()
    {
        cout << "~A()" << endl;
    }
};
int main()
{
    A aa1(1);  //标准的构造函数调用,定义有名对象
    A aa2 = 2; //隐式类型转换,现代编译器优化后,也是直接调用的构造函数
    A(3);      //构造匿名对象,生命周期只在这一行,这一行结束后就会调用析构函数
    A aa3(2);	//对比观察更明显
    system("pause");
    return 0;
}
打印结果:
A(int a)//construction
A(int a)//construction
A(int a)//construction
~A()
A(int a)//construction
请按任意键继续. . .
~A()
~A()
~A()

什么情况下能用到匿名对象呢?

定义一个对象要用,但是仅在这一行用,其他地方不用

//纯碎想调用函数
A().f();

vector<A> v;
A aa1(1);
v.push_back(aa1);

v.push_back(A(3));
v.push_back(A(4));
v.push_back(A(5));

explicit关键字

构造函数不仅可以构造和初始化对象,对于单个参数的构造函数,还支持隐式类型转换。
对于单参数的自定义类型来说 A aa3 = 2; 这种代码的可读性不是很好,我们若是想禁止单参数构造函数的隐式转换,可以用关键字explicit来修饰构造函数

explicit A(int a) // construction
    : _a(a)
    {
        cout << "A(int a)//construction" << endl;
    }

image-20220303123415963

12.static成员

声明为static的类成员称为类的静态成员。
用static修饰的成员变量,称之为静态成员变量;
用static修饰的成员函数,称之为静态成员函数。

特性

1.静态区,类内声明

class Test
{
private:
	static int _n;
};
int main()
{
	cout << sizeof(Test) << endl;//1
	return 0;
}

结果,因为静态成员_n是存储在静态区的,属于整个类,也属于类的所有对象。所以计算类的大小或是类对象的大小时,静态成员并不计入其总大小之和。

练习:计算A定义了多少个对象

class A
{
    public:
    //every time construction or copy_construction --> ++_n 
    A()
    {
        ++_n;   
    }
    A(const A&a)
    {
        ++_n;
    }
//private:
//这只是声明,不在构造函数初始化,在类外全局位置初始化
    static int _n;
};
//静态成员变量定义初始化
int A::_n = 0;
void f(A a)
{
    
}
int main(int argc, char const *argv[])
{
    A a1;
    A a2;
    A();
    f(a1);//call copy constructions
    cout << sizeof(A) << endl;
    cout << A::_n << endl; // if public  4  a1 1times a2 1times A()1times f(a1)1times
    cout << a1._n << endl; // if public  4
    cout << a2._n << endl; // if public  4
    cout << A()._n << endl; // if public 5 A()._n create an anonymous object call constructions
    system("pause");
    return 0;
}
class A
{
    public:
    //every time construction or copy_construction --> ++_n 
    A()
    {
        ++_n;   
    }
    A(const A&a)
    {
        ++_n;
    }
    int GetN()
    {
        return _n;
    }
private:
//This is just a declaration that it is not initialized in the constructor, but in the global location outside the class
    static int _n; //only the first can change
    //Special case, not restricted by access qualifiers
};
//静态成员变量定义初始化
int A::_n = 0;
void f(A a)
{
    
}
int main(int argc, char const *argv[])
{
    A a1;
    A a2;
    A();
    f(a1);//call copy constructions
    cout << sizeof(A) << endl;
    // cout << A::_n << endl; // if public  4  a1 1times a2 1times A()1times f(a1)1times
    // cout << a1._n << endl; // if public  4
    // cout << a2._n << endl; // if public  4
    // cout << A()._n << endl; // if public 5 A()._n create an anonymous object call constructions
    
    cout << a1.GetN() << endl; // if private 4
    cout << A().GetN() << endl; // if private 5
    system("pause");
    return 0;
}

2.类外定义

静态成员变量必须在类外定义,定义时不添加static关键字

class Test
{
private:
	static int _n;
};
// 静态成员变量的定义初始化
int Test::_n = 0;

注意:这里静态成员变量_n虽然是私有,但是我们在类外突破类域直接对其进行了访问。这是一个特例,不受访问限定符的限制,否则就没办法对静态成员变量进行定义和初始化了

3.无this指针

静态成员函数没有隐藏的this指针,不能访问任何非静态成员

class Test
{
public:
	static void Fun()
	{
		//cout << _a << endl; //error不能访问非静态成员
		cout << _n << endl; //correct
	}
private:
	int _a; //非静态成员
	static int _n; //静态成员
};

注意:含有静态成员变量的类,一般含有一个静态成员函数,用于访问静态成员变量

4.访问静态成员变量

public
#include <iostream>
using namespace std;
class Test
{
public:
	static int _n; //公有
};
// 静态成员变量的定义初始化
int Test::_n = 0;
int main()
{
	Test test;
	cout << test._n << endl; //1.通过类对象突破类域进行访问
	cout << Test()._n << endl; //3.通过匿名对象突破类域进行访问
	cout << Test::_n << endl; //2.通过类名突破类域进行访问
    //突破类域:编译器编译时到类中去找
	return 0;
}
private
#include <iostream>
using namespace std;
class Test
{
public:
	static int GetN()
	{
		return _n;   //既然你的成员是私有的,那么我就通过公有的函数来返回你私有的成员
	}
private:
	static int _n;
};
// 静态成员变量的定义初始化
int Test::_n = 0;
int main()
{
	Test test;
	cout << test.GetN() << endl; //1.通过对象调用成员函数进行访问
	cout << Test().GetN() << endl; //2.通过匿名对象调用成员函数进行访问
	cout << Test::GetN() << endl; //3.通过类名调用静态成员函数进行访问
    //如果没有Test:: 编译器就回去全局找GetN()
	return 0;
}

5.访问级别

静态成员和类的普通成员一样,也有public、private和protected这三种访问级别。
所以当静态成员变量设置为private时,尽管我们突破了类域,也不能对其进行访问。

注意区分两个问题:
1、静态成员函数可以调用非静态成员函数吗?
 2、非静态成员函数可以调用静态成员函数吗?

问题1:不可以。因为非静态成员函数的第一个形参默认为this指针,而静态成员函数中没有this指针,故静态成员函数不可调用非静态成员函数。

static int GetN()
{
    //_a = 1; //this->_a
    //f(); // this->f(); 没有this指针
    return _n;
}

问题2:可以。因为静态成员函数和非静态成员函数都在类中,在类中不受访问限定符的限制。

13.C++11成员初始化新花样

C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量一个缺省值

class A
{
public:
	void Print()
	{
		cout << _a << endl;
		cout << _p << endl;
	}
private:
	// 非静态成员变量,可以在成员声明时给缺省值。
	int _a = 10; 
	int* _p = (int*)malloc(4);
	static int _n; //静态成员变量不能给缺省值
};
class B
{
public:
    B(int x = 0)
        : _x(x)
    {
        cout << " B()" << endl;
    }

private:
    int _x;
};
// C++11 针对之前内置类型不做处理的缺陷做了改进
class A
{
private:
    // declaration
    int _a = 0;
    int *_p = nullptr; //这里不是初始化,只是缺省值
    int* arr = (int*)malloc(sizeof(int)*10);
    B _b = 6;//这其实是一个先构造再拷贝,但被编译器优化成直接构造了

public:
    A(int a = 1, int *p = nullptr)
        : _a(a), _p(p)//初始化给值了,就不会再去用声明时的缺省值
    {
    }
};

int main(int argc, char const *argv[])
{
    A aa; //会去调用默认构造函数
    system("pause");
    return 0;
}

image-20220304084715343

初始化列表是成员变量定义初始化的地方,你若是给定了值,就用你所给的值对成员变量进行初始化,你若没有给定值,则用缺省值进行初始化,若是没有缺省值,则内置类型的成员就是随机值。

14.友元

<< >>重载

内置类型对象能直接用cin cout输入输出,是因为库里已经重载好了的;
并且能自动识别类型,是因为他们构成了函数重载

image-20220223110326754

//d1 << cout; 这样调用才行,非常反直觉
//第一个参数是左操作数,第二个参数是右操作数
void operator<<(ostream& out)//void operator<<(Date* this, ostream& out)
{
    //cout传进来,out就是cout别名
    //内置类型可以直接用cout输出
    out << _year << "-" << _month << "-" << _day << endl;
}
int main(int argc, char const *argv[])
{
    Date d1(2022,2,22);
    const Date d2(2022,2,22);
    d1 << cout;
    system("pause");
    return 0;
}
//cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。

这样不符合要求,那就不要写成成员函数了,写成全局函数就行了

但写成全局该怎么访问私有成员变量呢?
通过声明友元函数即可!

class Date
{
public:
    friend void operator<<(ostream& out, const Date& d);
    Date(int year = 0, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};

void operator<<(ostream& out, const Date& d)
{
    out << d._year << "-" << d._month << "-" << d._day << endl;
}
int main(int argc, char const *argv[])
{
    Date d1(2022,2,22);
    const Date d2(2022,2,22);
    &d2;
    cout << d1;
    system("pause");
    return 0;
}

连续输入输出

cout << d1 的返回值又要去输出 d2,所以应该是ostream返回值,并且出了作用域还在,返回引用即可

ostream& operator<<(ostream& out, const Date& d)
{
    out << d._year << "-" << d._month << "-" << d._day << endl;
    return out;
}
int main(int argc, char const *argv[])
{
    Date d1(2022,2,22);
    const Date d2(2022,2,22);
    cout << d1 << endl << d2 << endl;
    system("pause");
    return 0;
}
istream& operator>>(istream& in, Date& d)
{
    in >> d._year >> d._month >> d._day;
    return in;
}
int main(int argc, char const *argv[])
{
    Date d1(2022,2,22);
    Date d2(2022,2,22);
    cin >> d1 >> d2;
    cout << d1 << endl << d2 << endl;
    system("pause");
    return 0;
}

关于cin cout重载,如果不声明友元函数,可以通过在类外调用类里面的公有成员函数进而获得私有的成员变量
或者也可以直接把成员变量public

//调用公有函数从而访问私有变量
int GetYear() const//GetYear需要用const修饰,因为下面的operator<<用const修饰
{
    return _year;
}
void operator<<(ostream &out, const Date &d)
{
    //out << d._year << "-" << d._month << "-" << d._day << endl;
    out << d.GetYear() << "-" << d._month << "-" << d._day << endl;
}

概念

友元分为:友元函数和友元类
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

友元函数

友元函数说明:
 1、友元函数可以访问类是私有和保护成员,但不是类的成员函数。
 2、友元函数不能用const修饰。(const修饰的是this指针,里面没有this指针)
 3、友元函数可以在类定义的任何地方声明,不受访问限定符的限制。
 4、一个函数可以是多个类的友元函数。
 5、友元函数的调用与普通函数的调用原理相同。

友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中非公有成员。

class A
{
	// 声明B是A的友元类  可以理解为B是A的好朋友,A的任何东西B都可以访问,包括私有类型的。
	friend class B;//可以写在类里面任意位置,但一般写最前面
public:
	A(int n = 0)
		:_n(n)
	{}
private:
	int _n;
};
class B
{
public:
	void Test(A& a)
	{
		// B类可以直接访问A类中的私有成员变量  
		cout << a._n << endl;
	}
};

友元类说明

  1. 友元关系是单向的,不具有交换性。

    例如上述代码中,B是A的友元类,所以在B类中可以直接访问A类的私有成员变量,但是在A类中不能访问B类中的私有成员变量。

  2. 友元关系不能传递。

    如果A是B的友元,B是C的友元,不能推出A是C的友元。

总结:

不建议多用友元,友元是一种破坏封装的行为

15.内部类

概念:

如果一个类定义在另一个类的内部,这个内部类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。

注意内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元

特性:

  1. 内部类可以定义在外部类的public、protected、private都是可以的。
  2. 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
  3. sizeof(外部类)=外部类,和内部类没有任何关系。
class A
{
public:
    A(int k, int h)
    {
        this->_k = k;
        this->_h = h;
    }

private:
    static int _k;
    int _h;

public:
    class B // B天生就是A的友元
    {
    public:
        void foo(const A &a)
        {
            cout << _k << endl;   // OK  a._k也ok 静态成员变量
            cout << a._h << endl; // OK
        }
    };
};

int A::_k = 1;

int main()
{
    A::B bb;
    bb.foo(A(2, 10));//这样去访问内部类的成员函数
    system("pause");
    return 0;
}

16.封装&面向对象

再次理解封装

C++是基于面向对象的程序,面向对象有三大特性:封装、继承、多态

C++通过类,将一个对象的属性与行为结合在一起,(成员变量和成员函数放到类里面)使其更符合人们对于一件事物的认知,将属于该对象的所有东西打包在一起。通过访问限定符的将其部分功能开放出来与其他对象进行交互,而对于对象内部的一些实现细节,外部用户不需要知道,知道了有些情况下也没用,反而增加了使用或者维护的难度,让整个事情复杂化。
本质就是一种管理

对比感受

//用C实现一个栈
struct StackC
{
    int *a;
    int _top;
    int _capacity;
};
void StackInit(struct StackC *ps, int n);
void StackDestroy(struct StackC *ps);
void StackPush(struct StackC *ps, int x);

void TestStackC()
{
    struct StackC st;
    StackInit(&st, 4);
    StackPush(&st, 1);
    StackPush(&st, 2);
    StackPush(&st, 3);
    StackPush(&st, 4);
    st.a[st._top++] = 5;//不合法的插入,可能越界了

    StackDestroy(&st);
}
// C++实现一个栈
class StackCpp
{
private:
    int *a;
    int _top;
    int _capacity;

public:
    StackCpp(int n = 4)
    {
        //...
    }
    ~StackCpp()
    {
        //../
    }
    void Push(int x)//也还是2个参数,只不过有个是隐含的this指针
    {
        //...
    }
};

void TestStackCpp()
{
    StackCpp st;//自动构造,自动析构
    st.Push(1);
    st.Push(2);
    st.Push(3);
    st.Push(4);
    //st._a; 无法这样操作
}

再次理解面向对象

可以看出,面向对象其实是在模拟抽象映射现实世界:
image-20220304090835738

练习

求1+2+…+n

循环,递归,公式等方法都被限制了

//不用if else 的递归
class Solution {
public:
    int sumNums(int n) {
        n && (n+=sumNums(n-1));
        return n;
    }
};

借助Add类变长数组创建对象时自动调用构造函数的特性

class Add
{
public:
    Add()
    {
        _ret += _i;
        _i++;
    }
    static int GetRet()
    {
        return _ret;
    }
    static void Init()
    {
        _i = 1;
        _ret = 0;
    }
private:
    static int _i;
    static int _ret;
};
int Add:: _i = 1;
int Add:: _ret = 0;//类外定义static成员变量
class Solution {
public:
    int sumNums(int n) {
        //因为有多个测试用例
        Add::Init();//每次进来时要重置_i _ret避免上一次结果残留
        Add arr[n];//构造函数就能调用n次了
        return Add::GetRet();
    }
};

成员变量 public

class Add
{
public:
    Add()
    {
        _ret += _i;
        _i++;
    }

    static int _i;
    static int _ret;
};
int Add:: _i = 1;
int Add:: _ret = 0;//类外定义static成员变量
class Solution {
public:
    int sumNums(int n) {
        Add::_ret = 0;//每次进来时要重置_i _ret避免上一次结果残留    直接调用公有成员变量
        Add::_i = 1;
        Add arr[n];
        return Add::_ret;
    }
};

声明友元类

class Add
{
    friend class Solution;
public:
    Add()
    {
        _ret += _i;
        _i++;
    }
private:
    static int _i;
    static int _ret;
};
int Add:: _i = 1;
int Add:: _ret = 0;//类外定义static成员变量
class Solution {
public:
    int sumNums(int n) {
        Add::_ret = 0;//每次进来时要重置_i _ret避免上一次结果残留    直接调用公有成员变量
        Add::_i = 1;
        Add arr[n];
        return Add::_ret;
    }
};

借助内部类

class Solution
{
    class Add // Add是Solution的友元
    {
    public:
        Add()
        {
            _ret += _i;
            _i++;
        }
    };

public:
    int sumNums(int n)
    {
        _ret = 0; //每次进来时要重置_i _ret避免上一次结果残留    直接调用公有成员变量
        _i = 1;
        Add arr[n];
        return _ret;
    }

private:
    static int _i;
    static int _ret;
};
int Solution::_i = 1;
int Solution::_ret = 0;

日期到天数转换

#include <iostream>
using namespace std;
int main()
{    
    int year, month, day;
    int md[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31};
    int total_day = 0;
    //读入数据
    cin >> year >> month >> day;
    if(year % 4 == 0 && year % 100 != 0 || year% 400 == 0)
        md[2] = 29;
    //按月份累加天数
    for(int i = 1; i < month; i++)
        total_day = total_day + md[i];
    total_day += day;
    cout << total_day << endl;
    return 0;
}

今天的总结就到这里了呢,请务必先收藏再食用,如果觉得对您有帮助,烦请点个赞。
鄙人小菜鸟一枚,如有纰漏烦请指出。
附GitHub仓库链接

评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值