类与对象(C++)

一.引子

C语言是面向过程,关注的过程。
C++基于面向对象的,关注的是对象。
用大白话就是点外卖:
C语言是关注外卖骑手如何接单,商家如何炒菜这个过程
C++是有几类对象参与工作,用户,商家,骑手。这三类群体关系
这里就开个引子,不用过多追究

二.类的引入

在C语言中定义结构体:

struct Stack
{
	int* a;
	int top;
	int capacity;
}

C++兼容结构体struct的用法,同时升级成了类
增添了以下几个功能:

类名代表类型在这里插入图片描述
类中可以定义函数在这里插入图片描述

关于结构体的定义,C++中更喜欢用class来替代。

三.类的定义(class)

class className
{
	//类体:由成员函数和成员变量组成
};  //这里分号要注意

class为类定义的关键字,ClassName为类的名字,{}中为类的主体
这里说以下类体的命名习惯:
引例:这里初始化赋值的时候year=year不知道哪个是哪个

class Date
{
	void Init(int year)
	{
		year = year;
	}
	int year;
};

所以在成员变量前面加个_就更好区分(C++语法里面没有规定,只是个人习惯)

class Date
{
	void Init(int year)
	{
		_year = year;
	}
	int _year;
};

然后运行的时候你就会发现调用不动函数
在这里插入图片描述
这里就由一个公有(pubilc)和私有(private)的问题:

访问限定符

在这里插入图片描述
1.pubilc修饰的成员在类外面可以直接被访问
2.protected和private修饰的成员在类外面不能被直接访问(目前这个阶段可以认为protected和private没有区别)
3.访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
class默认的访问权限是private
struct默认的访问权限是pubilc
所以想访问class中的数据需要加个pubilc:

class Date
{
public:
	void Init(int year)
	{
		_year = year;
	}
private:
	int _year;
};

int main()
{
	Date st1;
	st1.Init(1);
	return 0;
}

这里成员变量一般是私有,成员一般是公有(没有要求,只是习惯)
这里又引出来一个问题,类中的函数声明和定义如何写:

Stack.cppStack.htest.cpp
#include “Stack.h”
void Date::Init(int year)
{
_year = year;
}
#include
using namespace std;
class Date
{
public:
void Init(int year);
private:
int _year;
};
#include “Stack.h”
int main()
{
Date st1;
st1.Init(1);
return 0;
}

寻找定义的时候,会依次类域,局部域,全局域这样搜索,函数声明又在类域中,故在类域中寻找
又牵扯出一个问题:这里的_year是声明还是定义?

class Date
{
public:
	void Init(int year)
	{
		_year = year;
	}
	int _year;
};
int main()
{
	Date d1;//开空间了
	d1._year++;
	//Date::_year++;这是不可以的,因为是定义
	return 0;
}

这里的_year是定义,评判声明还是定义的标准是是否开空间

关于面向对象的三大特性–封装

面向对象的三大特性:封装,继承,多态
封装的本质就是一种管控:
1.C++中数据和方法都放进类里面
2.C++访问限定符去对成员进行限制,想给你访问的是公有,不想给你访问的是私有。
举个例子:故宫有游览区域和不可游览区域,可游览即位公有,不可游览即位私有,封装就是外面的墙把区域分开。
在这里插入图片描述

四.类的实例化

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

  1. 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类,并没有分配实际的内存空间来存储它。(面向对象)
  2. 实例化出的对象,对象占用实际的物理空间,存储类成员变量
    储存空间大小跟C语言中结构体struct的内存对齐规则一样,不同的是函数储存
    例子如下:用反汇编查看调用函数的地址
#include <iostream>

using namespace std;

class Date
{
public:
	void Print()
	{
		cout << _year << endl;
	}
private:
	int _year;
};

int main()
{
	Date d1;
	Date d2;
	d1.Print();
	d2.Print();
	cout << sizeof(d1) << endl;
	return 0;
}

输出:
在这里插入图片描述
发现内存就4个字节,就一个整型的大小,说明函数不占空间。
反汇编:
在这里插入图片描述
发现一模一样,所以函数地址是存放在公共区域,谁调用谁去公共区域找。
画个图理解一下:
在这里插入图片描述
看两个特殊的类

类中仅有成员函数的大小

在这里插入图片描述

类中什么都没有—空类

在这里插入图片描述
仅有函数和空类比较特殊,因为定义一个a,得有空间去存储识别,所以编译其给了空类一个字节来唯一识别这个类的对象
综上所述:类只有实例化才能存储数据,占用空间。

五.this指针

引子:为什么能分别输出2023,2024?

#include <iostream>

using namespace std;

class Date
{
public:
	void Init(int year)
	{
		_year = year;
	}
	void Print()
	{
		cout << _year << endl;
	}
private:
	int _year;
};

int main()
{
	Date d1;
	Date d2;
	d1.Init(2023);
	d1.Print();
	d2.Init(2024);
	d2.Print();
	return 0;
}

其实隐藏了一个this指针。可以把打印函数看成:
在这里插入图片描述
完整的其实是:因为this指针不可被修改
在这里插入图片描述
在这里插入图片描述

特点:

1.在形参和实参的地方不能显示写(所以上面的图是报错的)
2.在函数内部可以使用
在这里插入图片描述

练习

一:

this 指针存放在哪里?
a.堆 b.栈 c.静态区 d.常量区 e.对象里面
这里e肯定不对,因为可以从sizeof()成员大小看出,没有this,就比如上面的Date的大小就是4字节
d的常量区也不对,C++中const的变量可能不在常量区:
在这里插入图片描述
c静态区不可能,因为只有static和全局变量在静态区
a堆只有malloc realloc这种才是在堆中
所以在栈上,因为他是一个形参,形参在函数中,函数是存在栈中
有些编译器也可能用寄存器去传递(比如VS),因为this指针运用的频繁。
在这里插入图片描述

二:

以下代码会是:1.编译错误 2.运行错误 3.正常运行

#include <iostream>

using namespace std;

class A
{
public:
	void Print()
	{
		cout << "void Print()" << endl;
	}
};

int main()
{
	A a1;
	A* p1 = &a1;
	p1->Print();
	A* p2 = nullptr;
	p2->Print();
	return 0;
}

答案是正常运行
在这里插入图片描述
解析:语法没有错误,所以不可能是编译错误。看似p->解引用了实则没有,函数不在对象里
在这里插入图片描述

三:

下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行

#include <iostream>

using namespace std;

class A
{
public:
	void PrintA()
	{
		cout<<_a<<endl;
	}
private:
	int _a;
};

int main()
{
	A* p = nullptr;
	p->PrintA();
	return 0;
}

这里是运行崩溃,因为cout << this->_a << endl;这里this是空指针,解引用是空指针崩了

四:

下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行

class A
{
public:
	void PrintA()
	{
		cout << "Print()" << endl;
	}
private:
	int _a;
};

int main()
{
	A* p = nullptr;
	(*p).PrintA();
	return 0;
}

这里是正常运行,编译器不会解引用,编译器认为解引用没有意义

六.6个默认成员函数

在这里插入图片描述

构造函数:

祖师爷怕忘记初始化函数,所以发明了构造函数:

特征:

1.函数名与类名相同
2.无返回值(不是void,就不需要写)
3.对象实例化时编译器自动调用对应的构造函数
4.构造函数可以重载
例子:

#include <iostream>

using namespace std;

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

	Date(int year,int month,int day)//构造函数重载
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day  << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	d1.Print();
	Date d2(2024,3,10);//在对象后面跟参数
	d2.Print();
	return 0;
}

这里定义对象 d1的时候没有写成Date d1();是因为d1可能是函数,会混。
这样写有些麻烦,写成半缺省更方便:

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

但是半缺省和下面这个代码不能一起写,会出现不知道调哪个函数的情况

Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}

那就有人问上面的例子中构造函数能不能只写这个:

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

是不行的,画个图:(由于函数参数不一样,所以调用的构造函数不一样)
在这里插入图片描述
构造函数最主要的特性:默认成员函数,不写编译器会生成一个。
例子:

#include <iostream>

using namespace std;

class Date
{
public:
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day  << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	d1.Print();
	return 0;
}

在这里插入图片描述
会发现对其中的数据没有处理,是因为:
默认生成的构造函数,对于内置类型不做处理,自定义类型回去调用他的默认构造函数
如果我们没有写任何一个构造函数,编译器才会自动生成的,如果写了就不会生成
内置类型/基本类型: int/char/double/指针等 语言自己定义的类型
自定义类型: struct/class
这里验证以下自定义类型调用他的默认构造函数:

class A
{
public:
	A()
	{
		cout << "A" << endl;
	}
private:
	int _a;
};

class B
{
public:
	int _a;
	A _aa;
};

int main()
{
	B b1;
	cout << b1._a << endl;
	return 0;
}

在这里插入图片描述
虽然构造函数编译器会自己生成,但是绝大数场景下面都需要自己实现构造函数(队列的实现不用写)
C++11 委员会对这个语法进行打补丁,在声明的位置个缺省值(可能也觉得对内置类型不做处理不合理)
例子:

#include <iostream>

using namespace std;

class Date
{
public:
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day  << endl;
	}
private:
	int _year = 1;
	int _month = 2;
	int _day;
};

int main()
{
	Date d1;
	d1.Print();
	return 0;
}

在这里插入图片描述
注意这里是缺省值(和函数的缺省值差不多),和构造函数不冲突
默认构造函数有三种类型:(这三种默认构造函数只能由一个)
1.无参构造函数
2.全缺省构造函数
3.没写时编译器默认生成的构造函数
一般情况下:建议提供全缺省

析构函数

对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
就与堆free类似

特性:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
    验证:
#include <iostream>

using namespace std;

class Date
{
public:
	Date(int year = 1,int month = 1,int day = 1)//构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}
	~Date()
	{
		cout << this << endl;//析构函数
	}
private:
	int _year;
	int _month;
	int _day;
};

void func()
{
	Date d2;
}

int main()
{
	Date d1;
	func();
	return 0;
}

通过调试可以看出d2出了func就调用了析构函数,并且在析构函数中能使用this
在这里插入图片描述
这里有一个小知识点:因为创建在栈里,所以先创建的函数被压在栈底,先创建的函数后调用析构函数
在这里插入图片描述
默认生成的析构函数跟构造函数类似,对内置类型不做处理,对自定义类型调用他的析构(因为堆是否释放问题计算机不能解决)

生命周期:

以下代码测试的是谁先调用析构函数

#include <iostream>

using namespace std;

class Date
{
public:
	Date(int year)
	{
		_year = year;
	}
	~Date()
	{
		cout << "~Date()->" << _year <<  endl;
	}
private:
	int _year;
};

Date d6(6);
static Date d7(7);

void func()
{
	Date d4(4);
	static Date d5(5);
}

int main()
{
	Date d1(1);
	Date d2(2);
	static Date d3(3);
	func();
	return 0;
}

在这里插入图片描述
局部对象(后定义先析构) -> 局部的静态 -> 全局对象 (后定义先析构)

拷贝构造函数

相当于ctrl+C,ctrl+V。
例子:

#include <iostream>

using namespace std;

class Date
{
public:
	Date(int year = 1,int month = 1,int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2024,3,14);
	Date d2(d1);
	return 0;
}

可以看出d2拷贝了d1
在这里插入图片描述
重现拷贝构造功能之前先说一个结论:C++规定自定义的类型都会调用拷贝构造
举个例子:下面是拷贝构造,会发现参数部分用到是引用

Date(Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

这里解释一下为什么要用引用:
这是函数调用,参数部分用的是形参拷贝:

void func1(Date d)
{		
}

调试的时候就会发现
先走拷贝函数才进到func1函数,当然反汇编也能看出来,这是做了优化的,要不然就是无限递归
在这里插入图片描述
这是因为自定义都会调用拷贝构造,形成新的拷贝构造,画个图:(这样会无限递归下去)
在这里插入图片描述
然后这里又出现一个权限放大的问题:别名就是本身,不是形参,所以可以对参数进行改动:
在这里插入图片描述
这是一个很严重的权限放大,所以要加上const:想要它只读,就只能只读,权限平移
在这里插入图片描述
这里还有一点,拷贝构造也是构造,所以不会默认生成构造函数:
在这里插入图片描述
当然也可以加关键字强制生成默认构造函数:
在这里插入图片描述

特性:

1.拷贝构造函数是构造函数的一个重载形式
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用
3.若未显示定义,编译器会生成默认构造函数。默认的拷贝构造函数对象对内置类型成员完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
但是浅拷贝并不完善:比如堆的拷贝

#include <iostream>

using namespace std;

class Heap
{
public:
	Heap(int size = 4)
	{
		_size = size;
		_a = (int*)malloc(sizeof(int)*size);
	}
private:
	int _size;
	int* _a;
};

int main()
{
	Heap hp1;
	Heap hp2(hp1);
	return 0;
}

可以通过调试看出,拷贝的地址一样,并没有再次开堆,hp2只拷贝了hp1存储的堆地址,所以一旦hp1的堆释放了,hp2就成了野指针
在这里插入图片描述

所以得自己写拷贝构造函数:

Heap(const Heap& hp)
	{
		_size = hp._size;
		_a = (int*)malloc(sizeof(int)*_size);
	}

赋值运算符重载

运算符重载:

自定义类型无法实现比较,并且为了增强代码的可读性,所以祖师爷发明了运算符重载,运算符重载是具有特殊函数名的函数
引例:

#include <iostream>

using namespace std;

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

bool DateEqual(const Date& x, const Date& y)
{
	return x._year == y._year
		&& x._month == y._month
		&& x._day == y._day;
}

int main()
{
	Date d1(2024,1,1);
	Date d2(2024,1,1);
	cout << DateEqual(d1, d2) << endl;
	return 0;
}

这样一看感觉还行,但是大于小于等运算符在函数命名的时候很麻烦,而且调用函数时也很麻烦,所以有了下面的语法:operator+运算符做函数
在这里插入图片描述
这不是最终的形式:

bool operator==(const Date& x, const Date& y)
{
	return x._year == y._year
		&& x._month == y._month
		&& x._day == y._day;
}

int main()
{
	Date d1(2024,1,1);
	Date d2(2024,1,1);
	cout << ( d1 == d2 ) << endl;//  编译器会自动转成operator==(d1, d2)
	return 0;                    //这里加()是因为<<的优先级大于==
}

这里有个坑,就是这里:把变量设成公有,外面才能访问
在这里插入图片描述
这里讲一个解决办法:直接写进类里面:

class Date
{
public:
	Date(int year = 0,int month = 0,int day = 0)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	bool operator==(const Date& y)//注意这里面有个this指针,所以就写一个参数
	{
		return _year == y._year
			&& _month == y._month
			&& _day == y._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

所以要调用函数就可以写成:

int main()
{
	Date d1(2024,1,1);
	Date d2(2024,1,1);
	cout << d1.operator==(d2) << endl;
	return 0;
}
注意

1.不能通过连接其他符号来创建新的操作符(C/C++语法中存在):比如operator@
2.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数未隐藏的this指针
3. .* :: sizeof ?: . 以上5个运算符不能重载。

赋值运算符重载

跟拷贝构造有所区分:
在这里插入图片描述
语法:

void operator=(const Date& d)
{
	_year = d.—_year;
	_month = d._month;
	_day = d._day;
}

这里不仅仅支持d1=d2,还可以d1=d2=d3;想要连续赋值就得改一下上面的代码

Date operator=(const Date& d)
{
	_year = d.—_year;
	_month = d._month;
	_day = d._day;
	return *this;
}

这里还有一个问题,return this,返回的是this的拷贝,由于是Date类型,所以还要调用拷贝构造,返回值是引用就解决了。

Date& operator=(const Date& d)
{
	_year = d.—_year;
	_month = d._month;
	_day = d._day;
	return *this;
}

有的时候有人可能会写d1 = d1,所以要加个判断:

Date& operator=(const Date& d)
{
	if(this != &d)//这里是取地址
	{
		_year = d.—_year;
		_month = d._month;
		_day = d._day;
		return *this;
	}
}

特性跟拷贝构造类似:
1.赋值运算符只能重载成类的成员函数不能重载成全局的。(类中如果没有就会生成一个默认构造函数,又写一个全局函数,会造成歧义)
2.用户没有显式实现时,编译器会自动生成一个默认赋值运算符重载,以值的方式逐字节拷贝。

前置++和后置++重载:

因为语法规定:必须operate+运算符,所以要特殊处理前置和后置++,强行增加一个int形参,构成重载区分:

//前置++
Date& operator++()
{
	return (*this += 1);
}
//后置++
Date& operator++(int)//这里只能是int
{
	Date tmp(*this);
	*this += 1;
	return tmp;
}

显示引用就是:

int main()
{
	Date d1;
	d1.operator();//前置+=
	d1.operator(0);//后置++,这里()里面是几都没关系,区分函数用
	return 0;
}

关于流插入与流输出的运算符重载:

cout是全局ostream库里面的对象
在这里插入图片描述
支持任意类型的输出,库里面有内置类型:
在这里插入图片描述
但是自定义类型得自己写:

#include <iostream>

using namespace std;

class Date
{
public:
	Date(int year = 0,int month = 0,int day = 0)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void operator<<(ostream& out)
	{
		out << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d(2024,3,19);
	cout << d;//这里编不过去
	return 0;
}

是因为,operator+操作符,然后在类内定义函数,所以有隐藏的this指针,所以想调用只能写成:
在这里插入图片描述
然后会cout的发现方向反了,但由于类中的this指针干扰所以只能写在类外面,但是外面又调用不了类中的私有变量,所以要用到友元函数
完整版的如下:

#include <iostream>

using namespace std;

class Date
{
public:
	Date(int year = 0,int month = 0,int day = 0)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	friend void operator<<(ostream& out,const Date& d);//友元函数
private:
	int _year;
	int _month;
	int _day;
};
void operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}
int main()
{
	Date d(2024,3,19);
	cout << d;
	return 0;
}

只能传递一个是不够的,要做到cout << d1 << d2这样连续流插入,就要加个返回值。让cout << d1完成后返回cout,后cout << d2
代码如下:

ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	return out;
}

流提取在istream中:
在这里插入图片描述
自定义类型流提取函数:

istream& operator>> (istream& in,Date& d)//这里不能用const,因为输入的要放进d里
{
	in >> d._year >> d._month >> d._day;
	return in;
}

拷贝构造函数和operator=的一个误区

以下代码中:aa2=aa1是拷贝构造还是operator=

#include <iostream>

using namespace std;

class A
{
public:
	A(int a)
		:_a(a)
	{}
	A(const A& B)
	{
		_a = B._a;
	}
	A& operator=(A& B)
	{
		_a = B._a;
		return *this;
	}
private:
	int _a;
};
int main()
{
	A aa1(1);
	A aa2 = aa1;
	return 0;
}

通过调试可以知道是拷贝构造,在语法上,拷贝构造是已存在同类型的对象,要初始化创建新的对象;在底层上本身aa2就没被创建出来,operator中this指针就没有
在这里插入图片描述
下面这个aa2=aa1走的是拷贝构造还是operator=

int main()
{
	A aa1(1);
	A aa2(2);
	aa2 = aa1;
	return 0;
}

因为运算符重载是面对已经存在的对象,一个拷贝给另一个
在这里插入图片描述

关于const

先看以下代码为什么不能运行:

#include <iostream>

using namespace std;

class Date
{
public:
	Date(int year = 0,int month = 0,int day = 0)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	const Date d(2024,3,19);
	d.Print();
	return 0;
}

这是因为const修饰的d只能读,不能写,但是在内里面的*this是隐藏的,加不了const,所以祖师爷为了解决问题就让在函数后面加const

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

权限扩大不可以,但是缩小和平移是可以的,下面代码就是权限缩小

int main()
{
	Date d(2024,3,19);//const Date d(2024,3,19);就是权限的平移
	d.Print();
	return 0;
}

取地址自定义类型:

这两个函数一般不用重定义,编译器会默认生成

#include <iostream>

using namespace std;

class A
{
public:
	A(int a = 0)
	{
		_a = a;
	}
	A* operator&()
	{
		return this;
	}
	const A* operator&() const
	{
		return this;
	}
private:
	int _a;
};

int main()
{
	A aa1;
	const A aa2;
	cout << &aa1 << endl;
	cout << &aa2 << endl;
	return 0;
}

基本用不上重载取地址,如果你想让别人访问不到真正的地址可以重载!!!

A* operator&()
	{
		int a = 10;
		return (A*)&a;
	}

这样就就会返回a的地址,别人查bug就很难查出来了

七.再谈构造函数:

构造函数还有一些缺陷,就比如以下代码中的const类型无法定义:
在这里插入图片描述
如果是数组一类的话定义也很麻烦,所以祖师爷发明了初始化列表:初始化列表是每个成员变量定义初始化的为位置

class Date
{
public:
	Date(int year, int month, int day)
		:_n(1)
		, _year(year)
		, _month(month)
		,_day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
	const int _n;
};

有很多种初始化的方法了,这里介绍一下区别:
根据调试可以知道,按定义变量的顺序依次初始化,在初始化列表中没有初始化的变量时才会走缺省值,例如下:

#include <iostream>

using namespace std;

class Date
{
public:
	Date(int year, int month, int day)
		:_n(1)
		, _year(year)
	{}
private:
	int _year = 1;//初始化_year的时候会走初始化列表
	int _month = 1;
	int _day = 1;
	const int _n;
};

int main()
{
	Date d(2024,3,20);
	return 0;
}

在这里插入图片描述
当构造函数和初始化列表共同存在时:

#include <iostream>

using namespace std;

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

int main()
{
	Date d(2024,3,20);
	return 0;
}

通过调试可以知道先走初始化列表,然后走构造函数,可以理解为初始化列表负责初始化,构造函数负责改值:

在这里插入图片描述在这里插入图片描述

总结就是初始化列表和缺省值为先,然后是构造函数
注意:初始化列表每个成员变量最多初始化一次,和构造函数可以多次赋值不一样
原则上能用初始化列表就用初始化列表,因为const和引用必须走初始化列表,引用必须在定义的时候初始化
还有就是对自定义类型初始化做处理很方便(尤其是没有默认构造函数的时候):

#include <iostream>

using namespace std;

class A
{
public:
	A(int a)
	{
		cout << "A(int a = 1)" << endl;
	}
};
class Date
{
public:
	Date(int year, int month, int day)
		: _year(1)
		,aa(1)//这里很方便
		, _month(1)
		,_day(day)
	{}
private:
	int _year;
	A aa;
	int _month;
	int _day;
};

int main()
{
	Date d(2024,3,20);
	return 0;
}

当然活学活用,混着用更好:比如这里进行对堆的检查(括号里面可以写函数)

class Date
{
public:
	Date(int year, int month, int day)
		: _year(0)
		, _month(0)
		, _day(0)
		, p((int*)malloc(sizeof(4)*10))
	{
		if (p == nullptr)
		{
			perror("malloc fail");
		}
	}
private:
	int _year = 2;
	int _month = 2;
	int _day = 2;
	int* p;
};

八.单参数构造函数支持隐式类型的转换:

引例:会发现可以拿=初始化
在这里插入图片描述
这是因为1构造了一个临时对象,再拷贝构造,举个简单的引例:(原理差不多)
在这里插入图片描述
当然这里拷贝构造时无法验证的,因为编译器优化,同一个表达式连续步骤的构造,一般会合二为一。
如果不想让隐式类型转换加个explicit

class Date
{
public:
	explicit Date(int year)
		:_year(year)
	{}
private:
	int _year;
};

应用于栈比较方便,例子如下:模拟栈,就看个形似就行

#include <iostream>

using namespace std;

class Date
{
public:
	Date(int year)
		:_year(year)
	{}
private:
	int _year;
};

class Stack
{
public:
	void Push(const Date& c)
	{
	
	}
private:
};

int main()
{
	Stack s;
	s.Push(1);//这里是不是方便很多了
	return 0;
}

关于多参数:

C++98是不支持多参数的,C++11后是支持的,如下:

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 d = { 2024,3,20 };
	return 0;
}

练习:

A. 输出1 1 B.程序崩溃 C.编译不通过 D.输出1 随机值

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

这里输出1和随机值,因为初始化顺序是按照定义的顺序来的,先_a2然后_a1,_a2初始化的时候_a1并没有赋值,所以_a2是随机值,_a1是1
所以在初始化函数的时候,要按照定义的顺序比较好

九.static

静态成员变量

引例:想统计构造函数用了几次:不想被对象外面的函数访问到,所以用类做封装

#include <iostream>

using namespace std;

class A
{
public:
	A()
	{
		_n++;
	}
	//不是属于某一个对象,属于所有对象,属于整个类
	//声明
	static int _n;
};

//定义
int A::_n = 0;

A Func()
{
	A aa3;
	return aa3;
}

int main()
{
	A aa1;
	A aa2;
	Func();
	cout << A::_n << endl;
	return 0;
}

这里_n的数值会因为编译器的不同所不同,VS19在debug的版本下是4,releas版本下是3,VS2在debug的版本下是3。
然后这里还有个问题,_n是公有外面才能访问,所以用到Get的方法:

#include <iostream>

using namespace std;

class A
{
public:
	A()
	{
		_n++;
	}
	int GetN()
	{
		return _n;
	}
private:
	static int _n;
};

int A::_n = 0;

A Func()
{
	A aa3;
	return aa3;
}

int main()
{
	A aa1;
	A aa2;
	Func();
	cout << aa1.GetN() << endl;
	return 0;
}

注意:1.静态成员变量一定要在类外进行初始化
2.static成员变量在对象生成之前生成

static成员函数

注意:静态的成员函数是没有this指针的,所以无法直接访问其余的成员变量,目的就是访问静态函数和静态变量的:
在这里插入图片描述
可以这样访问:直接指定类域

	cout << A::GetN() << endl;

十.友元

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

友元函数的特性

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

友元类

引例:这里Date能访问Time,但Time不能访问Date

#include <iostream>

using namespace std;

class Time
{
	friend class Date; //友元类
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 = 0, int month = 0, int day = 0)
	{
		_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;
};

十一.内部类

如果一个类定义在另一个类的内部,就叫做内部类
引例:看一下A的大小

#include <iostream>

using namespace std;

class A
{
public:
	class B
	{
	public:
	private:
		int _b;
	};
private:
	int _a;
};

int main()
{
	cout << sizeof(A) << endl;
	return 0;
}

A的大小为4,sizeof(外部类)=外部类,不会给内部类开辟空间
如果是想成8的话是下面这种形式:

#include <iostream>

using namespace std;

class B
{
private:
	int _b;
};
	
class A
{
private:
	int _a;
	B _bb;
};

在内部类中,B这个类受A类的类域的限制:

#include <iostream>

using namespace std;

class A
{
public:
	class B
	{
	public:
		void Print()
		{
			cout << "void Print()->B" << endl;
		}
	private:
		int _b;
	};
private:
	int _a;
};

int main()
{
	A::B bb;//这里受到类域的限制
	bb.Print();
	return 0;
}

还有一个特征:内部类天生就是外部类的友元类

class A
{
public:
	class B
	{
	public:
		void func(A* p)//这里可以访问到A中的变量
		{
			p->_a++;
		}
	private:
		int _b;
	};
private:
	int _a;
};

十二.匿名对象

引例:

#include <iostream>

using namespace std;

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};

int main()
{
	A aa1;//有名对象
	A();//匿名对象
	A(10);//匿名对象
	return 0;
}

通过调试可以确定匿名对象的生命周期只有一行
在这里插入图片描述
用处就是方便访问类中的函数,不用创建新的变量:

#include <iostream>

using namespace std;

class A
{
public:
	void Func()
	{
		cout << "Func()" << endl;
	}
};

int main()
{
	A().Func();
	return 0;
}
  • 20
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浅碎时光807

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值