C++ 类和对象

类和对象cpp 【类的成员函数和类的定义分开写】(截到人脸郭老师orz【注意这里的::表示不是全局函数,而是一个类的成员函数】 阅读详情

一、类的定义

1.1 类的定义格式

class 是用于定义类的关键字,以 Stack 类为例,类名紧跟关键字,大括号内部为类主体,书写时需要注意类定义结束的分号不可省略。

大括号内的内容统称为类成员,其中变量称作成员变量或属性,函数称作成员函数或方法;

为区分成员变量与局部变量,工程中普遍存在命名惯例,会为成员变量添加下划线前缀、后缀或是以 m 开头,该规范不属于 C++ 语法强制要求,具体遵循对应项目或公司编码标准;

C++ 拓展了 struct 的能力,既兼容 C 语言仅存放数据的用法,也可以像 class 一样定义成员函数,二者都能实现类的定义,但日常开发中更推荐使用 class;

另外直接在类内部完成实现的成员函数,会被编译器默认为内联函数。

#include<iostream>

using namespace std;

class stack
{
	void Push(int x)
	{ 
		//...
	}
	void pop()
	{
		//...
	}
	int top()
	{
		//...
	}

	int* a;
	int top;
	int capacity;
};

1.2 访问限定符

访问限定符是 C++ 实现面向对象封装的重要手段,我们可以借助类把对象的属性和方法整合在一起,再通过不同的访问权限,选择性向外部开放可用接口,提升代码的安全性与规范性。

C++ 共有public、protected、private三种访问权限:

public修饰的成员能在类外部直接访问;

protected和private修饰的成员都无法在类外直接访问,二者的差异要到继承场景中才会体现出来。

访问权限的生效范围从该限定符声明处开始,一直延续到下一个访问限定符出现为止,若后续没有新的限定符,权限范围会持续到类定义结束的大括号处。

还要注意默认权限的区别:用class定义类时,未显式添加访问限定符的成员默认是private权限;用struct定义类时,成员默认权限为public。

在日常开发的编码规范里,我们通常会把成员变量设置为private或protected做隐藏保护,只把需要对外提供调用能力的成员函数设置为public,以此落实封装的设计思想。

#include<iostream>

using namespace std;

class Stack
{
public:
	void Push(int x)
	{}
	void Pop()
	{}

	int Top()
	{
		return 0;
	}
private:
	int* a;
	int top;
	int capacity;
};

int main()
{
	Stack st;
	st.Pop();
	st.Push(1);
	//st.a;

	return 0;
}

1.3 类域

类会单独开辟专属的作用域,类内部所有成员均归属于该类域,若在类体外实现成员函数,必须借助::作用域解析运算符标注所属类名,以此明确该函数归属的类域;

类域会改变编译器标识符查找逻辑,以 Stack 类的 Init 函数为例,若定义函数时未标注 Stack::,编译器会将其识别为普通全局函数,无法检索到 array 这类类内成员从而触发编译报错,添加类域标识后编译器便知晓这是类成员函数,在当前作用域找不到对应变量时会自动进入类域完成查找。

二、实例化

2.1 实例化概念

实例化指依据自定义类的类型,在物理内存中创建实体对象的过程;类本身只是对对象的抽象描述,相当于一套数据模型,仅声明所需成员变量,不会分配任何内存空间,只有完成实例化生成对象时,系统才会为成员变量分配专属存储空间;同一个类能够实例化出多个相互独立的对象,每一个对象都会占用独立物理内存用于存放自身数据,我们可以将类类比建筑图纸,图纸仅规划房屋结构与功能,不存在实体建筑、无法承载实际内容,依托图纸建造出实体房屋才能投入使用,对应到代码中,类仅作为模板无法存储数据,经过实例化后的对象才具备内存空间、可保存各类成员数据。

struct Date
{
public:
	void Init(int year, int month, int day);

	int _year; // year_ m_year
	int _month;
	int _day;
};

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

int main()
{
    //实例化
	Date d2;
	Date d3;

	return 0;
}

2.2 对象的大小

类实例化后的对象仅存储自身独立的成员变量,不会保存成员函数相关数据;

成员函数编译后为指令集合,统一存放于程序代码段,若为每个对象单独存储函数指针会造成大量冗余开销,例如同一个类创建百个对象,便会重复存储百份完全一致的函数地址,极大浪费内存空间;

普通非多态场景下编译器在编译链接阶段即可确定成员函数的固定地址,调用时直接通过 call 指令跳转对应地址,无需在对象内部保存函数指针,仅动态多态场景需要在对象中存储函数地址。

类的内存对齐规则与C语言中学的结构体的内存对其规则一模一样:

  • 第一个成员在与结构体偏移量为 0 的地址处。
  • 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
  • 注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
  • VS 中默认的对齐数为 8
  • 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
  • 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

为什么要内存对齐呢?

CPU 读取内存不是按单个字节随便读的,而是整块批量读取(比如一次读 4 字节、8 字节),硬件总线只支持从对齐地址开始读取数据。

假设 CPU 一次能读 4 字节数据,如果 int 变量刚好放在 4 的倍数地址,CPU 一次总线请求就能完整取出这个 int;要是变量卡在 2、3 这种非对齐地址,一次读不全,就得发起两次内存读取,再拼接数据,读写速度直接减半。内存对齐就是把变量规整到对应整数倍地址,保证一次读取到位,大幅提升程序运行效率。

不少架构的 CPU(比如 ARM)根本不支持直接访问非对齐内存地址,一旦数据错位,程序会直接崩溃、触发硬件异常;x86 虽然能兼容非对齐访问,但会产生巨大性能损耗。对齐机制是为了代码能跨硬件平台稳定运行,避免报错。

CPU、内存控制器的电路在设计时,如果要求支持任意地址读取,电路逻辑会变得极度复杂、成本更高;规定对齐规则后,硬件只需要处理固定间隔的地址,芯片设计难度、制造成本都会下降。

所以,内存对齐本质是牺牲少量内存空间(填充对齐空白),换取更快的读写速度、更好的硬件兼容性,属于以空间换时间、适配硬件底层的设计。

如果类中没有成员变量,实例化的时候还是会分配一个字节,纯粹是为了占位标识对象存在。

2.3 this指针

同一个类的全部对象共用一套成员函数代码,函数本身分不清现在是哪个对象在调用自己,C++ 就用隐式的 this 指针解决这个区分问题。

编译代码时,编译器会偷偷给每个成员函数的参数最前面加上一个当前类的 const 指针,也就是 this。拿 Date 类的 Init 函数举例,我们写出来的函数看着只有年月日三个参数,底层真实原型其实是void Init(Date* const this, int year, int month, int day)。

平时我们在函数里直接写_year = year修改成员变量,底层会自动转换成this->_year = year,本质全靠 this 指针找到当前调用函数的对象数据。

语法上我们不能手动在传参、函数形参位置写出 this,这一步完全交给编译器自动处理,不过在函数内部写代码时,可以手动写上 this-> 来访问成员。

#include<iostream>
using namespace std;
class Date
{
public:
	// void Init(Date* const this, int year, int month, int day)
	void Init(int year, int month, int day)
	{
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}

	// void Print(Date* const this)
	void Print()
	{
		cout << this->_year << "/" << this->_month << "/" << _day << endl;
	}

private:
	// 这里只是声明,没有开空间
	int _year;
	int _month;
	int _day;
};

int main()
{
	// Date类实例化出对象d1和d2
	Date d1;
	Date d2;

	d1.Init(2024, 3, 31);
	d1.Print();

	d2.Init(2024, 7, 5);
	d2.Print();
	return 0;
}

2.4 C语言与C++实现栈的对比

面向对象有封装、继承、多态三大核心特性,对比 C 语言、C++ 两种栈的实现思路,就能直观理解封装的含义,二者底层存储、运算逻辑完全一致,只是代码组织与使用方式差别很大。

最大改动来自 C++ 的封装设计:它把栈的数据、操作函数全部收纳进同一个类,搭配访问权限控制外部访问,外部无法随便直接篡改内部数据。封装本质就是对数据做规范化管控,杜绝非法读写引发 bug,这也是 C++ 面向对象最关键的优势。

同时 C++ 提供大量简化语法,大幅降低使用成本:支持函数默认参数,不用每次调用都传全参数;成员函数会自动隐式传递 this 指针,不用像 C 语言那样手动传入栈结构体地址;定义类型时也无需 typedef,直接使用类名即可,写法更简洁。

三、类的默认成员函数

默认成员函数指我们没有手动编写、编译器会自动生成的类内函数,在不自定义任何成员函数的情况下,编译器会自动生成六个默认成员函数,其中前四个是学习重点,后两个取地址重载实用性很低,简单了解即可;C++11 标准又新增移动构造与移动赋值两个默认成员函数,相关内容后续再展开讲解。

学习这类函数可以抓住两个核心思路,一是搞清楚编译器自动生成的函数具备什么样的行为,能否满足我们的业务需求,二是如果默认生成的逻辑不符合预期,我们该如何手动重写自定义版本,顺着这两个方向学习,就能理清复杂的默认成员函数相关知识点。

3.1 构造函数

构造函数是类里一种特殊成员函数,它不负责给对象分配内存,核心作用是在创建对象时自动完成成员初始化,替代了 C 语言里手动写的 Init 初始化函数,不用我们手动调用,使用更省心。

它有 7 个关键特性:

  1. 函数名字必须和类名完全一致;
  2. 没有任何返回值,不用写 void 也不用写其他类型,这是语法硬性规定;
  3. 只要创建实例化对象,程序会自动匹配并调用对应的构造函数;
  4. 支持函数重载,可以写多个参数列表不同的构造函数;
  5. 若我们完全不手动写构造函数,编译器会自动生成一个无参默认构造;只要我们手动定义了任意构造函数,编译器就不再自动生成;
  6. 无参构造、全缺省参数构造、编译器自动生成的无参构造,这三类都属于默认构造函数,满足 “不传实参就能直接调用” 的条件,但三者有且仅有一个存在,无参和全缺省构造一起写会出现调用歧义。默认构造参数调用时不能加括号,否则会和函数声明看起来一样。
  7. 编译器自动生成的构造对内置类型(int、char、指针等)不会做可靠初始化,变量值随机;如果类内包含自定义类型成员,会自动调用该成员的默认构造完成初始化,若这个自定义类没有默认构造,代码直接报错,这类场景需要借助初始化列表处理。

补充区分:C++ 的数据分为内置原生类型与我们自己用 class/struct 定义的自定义类型,二者在默认构造初始化时处理逻辑不一样。

#include<iostream>
using namespace std;

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

int main()
{
	Date d1;
	d1.Print();

	Date d2(2024, 7, 10);
	d2.Print();

	Date d3(2024);
	d3.Print();

	return 0;
}

3.2 析构函数

析构函数和构造函数作用正好相反,它不负责销毁对象本身(局部对象出作用域时栈空间会自动回收),核心作用是在对象销毁前自动清理、释放对象占用的堆内存、文件句柄这类资源,相当于之前栈代码里手动写的 Destroy 销毁函数;像只存普通数字的 Date 类没有额外资源,就不需要自定义析构函数。

析构函数核心特点:

  1. 函数名格式固定,在类名前面加波浪号~;
  2. 没有任何参数、也没有返回值,不用标注 void;
  3. 一个类只能拥有一个析构函数,不手动编写的话,编译器会自动生成默认析构;
  4. 只要对象生命周期走到尽头,程序会自动调用析构,无需手动执行;
  5. 编译器自动生成的默认析构不会处理 int、char 这类内置类型成员,但会自动调用类内自定义类型成员的析构函数;
  6. 就算我们手动写了析构函数,类里的自定义类型成员依旧会自动执行自身的析构逻辑(也就是无论什么情况,对于自定义类型成员都会自动调用它的析构函数);
  7. 编写取舍规则:类内没有向堆申请资源时,直接用编译器默认生成的析构即可;如果存在动态内存、文件等资源申请,必须手动实现析构释放资源,否则会发生内存泄漏;
  8. 同一作用域创建多个局部对象,销毁顺序遵循后创建先销毁的规则。
#include<iostream>
using namespace std;

typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	// ...

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

3.3 拷贝构造

拷贝构造是一种特殊的构造函数,判定标准很简单:第一个参数是当前类类型的引用,其余所有参数都带有默认值,它本身也是普通构造函数的重载版本。拷贝构造有一条硬性语法要求,第一个参数只能用引用传递,如果写成值传递,调用时会持续触发自身拷贝逻辑,造成无限递归,编译器会直接报错;它也可以设置多个参数,但除第一个引用参数之外,剩下的参数都必须配置缺省值。在 C++ 规则里,只要出现自定义类对象的拷贝行为,就一定会调用拷贝构造,常见场景包括函数按值传递对象、函数按值返回对象,这两种情况都会自动走拷贝构造逻辑。

如果我们没有手动写出拷贝构造函数,编译器会自动生成一份默认拷贝构造,这份默认实现执行逐字节的浅拷贝逻辑,针对 int、char 这类内置类型成员会直接完整复制内存数据,遇到类内嵌套的自定义类型成员时,则会自动调用该成员自身的拷贝构造完成复制。我们可以根据类的资源占用情况判断是否需要自己实现拷贝构造,像 Date 类只存放普通内置变量、没有动态堆资源,或是 MyQueue 这类内部成员为其他自定义类的结构,依靠编译器默认生成的拷贝构造就可以正常工作,不用手动编写;但类似 Stack 这种内部存在指针指向堆内存的类,默认浅拷贝只会复制指针地址,新旧对象共享同一块堆空间,析构阶段会重复释放内存引发程序崩溃,这时就需要手动实现深拷贝,为新对象单独开辟堆内存并复制对应资源数据,这里有一个实用判断技巧:只要一个类手写了析构函数用于释放堆资源,就必须配套实现拷贝构造,没有资源管理需求则不需要。

另外我们需要区分值返回和引用返回带来的拷贝差异,函数采用值返回对象时会生成临时对象,全程调用拷贝构造产生额外性能开销;采用引用返回不会生成副本,只是返回原对象的别名,能省去拷贝步骤提升效率,但引用返回存在严格使用限制,绝对不能返回函数内部创建的局部对象,函数执行完毕后局部对象会直接销毁,此时返回的引用会变成非法野引用,风险等同于野指针,只有确定返回对象在函数结束后依旧存在,才适合使用引用返回优化代码。

#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}

	// st2(st1)
	Stack(const Stack& st)
	{
		cout << "Stack(const Stack& st)" << endl;

		// 需要对_a指向资源创建同样大的资源再拷贝值
		_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		memcpy(_a, st._a, sizeof(STDataType) * st._top);
		_top = st._top;
		_capacity = st._capacity;

	}
private:
	STDataType* _a;
	int _capacity;
	int _top;
};

int main()
{
	Stack st1;
	//下面两种方式均为拷贝构造
	Stack st2 = st1;
	Stack st3(st1);
	return 0;
}

四、赋值运算符重载

4.1 运算符重载

4.1.1运算符重载基础概念

当我们自定义类的对象想要使用加减、等于这类运算符时,C++ 提供运算符重载机制,让我们自定义运算符在类身上的行为。只要类对象执行运算符操作,编译器就会自动去找对应的重载函数,找不到就直接编译报错。

运算符重载本质是一类特殊函数,函数名固定格式为operator+运算符符号,和普通函数一样支持返回值、参数列表与函数体。 参数数量遵循运算符操作数规则:一元运算符只需要 1 个参数,二元运算符需要 2 个参数;二元运算符会把左边参与运算的对象传给第一个参数,右边对象传给第二个参数。 如果把重载函数写成类的成员函数,左边运算对象会自动交给隐式的this指针,因此成员形式的运算符重载,参数个数会比运算符操作数少一个。 另外重载不会改变运算符原本的优先级、左右结合顺序,和内置类型运算规则保持一致。

4.1.2 运算符重载强制约束规则

  1. 不能创造语法里不存在的全新运算符,比如不能写operator@这种未定义符号。
  2. 有 5 个运算符完全禁止重载,分别是  .、.*、::、sizeof、?:,属于笔试选择题高频考点,需要牢记。
  3. 重载函数参数必须至少包含一个自定义类类型,不允许修改 int、double 等原生内置类型的运算符逻辑,比如不能重载两个 int 相加的 operator+。
  4. 只重载贴合业务逻辑的运算符,无实际作用的重载只会增加代码负担,例如日期类重载相减operator-可以计算日期间隔,有实际意义;重载加法operator+没有业务价值,不建议实现。

4.1.3 特殊运算符重载处理方案

  1. 自增运算符++区分前置、后置 前置 ++ 和后置 ++ 都对应函数名operator++,编译器无法自动区分,语法规定后置 ++ 重载函数额外增加一个 int 类型形参,依靠函数重载区分两种写法。
  2. 流运算符<<输出、>>输入不能作为类成员函数;成员函数第一个参数默认是 this 指针,会占据运算符左侧位置,调用时会变成对象<<cout,违背日常书写习惯。 正确做法是将operator<<、operator>>实现为全局函数,把 ostream/istream 流对象放在第一个参数位置,自定义类对象作为第二个参数,就能写出符合阅读习惯的cout << 对象。

4.2 赋值运算符重载

赋值运算符重载是编译器自动生成的默认成员函数,负责对两个已经存在的对象完成拷贝赋值,要区分开它和拷贝构造的使用场景:拷贝构造是用已有对象初始化一个全新待创建对象,赋值重载则针对两个已经创建完毕的对象完成赋值操作。

4.2.1 赋值运算符重载核心规则

  1. 函数形式限制 赋值运算符重载只能作为类的成员函数,不能写成全局函数;参数推荐使用const修饰的本类引用,若采用值传递传参,会额外触发对象拷贝,产生多余性能开销。
  2. 返回值规范 必须设置返回值,最优写法为返回当前类引用。引用返回可以避免生成临时对象、提升效率,同时只有返回自身引用,才能支持a=b=c这种连续赋值语法。
  3. 默认生成逻辑 如果我们没有手动实现赋值重载,编译器会自动生成默认版本。默认赋值重载执行浅拷贝逻辑:内置类型成员逐字节复制,自定义类型成员会自动调用自身的赋值重载完成赋值。

4.2.2 什么时候需要手动实现深拷贝赋值重载

  1. 无需手动实现:类中只有 int、char 等内置类型,没有堆动态资源(如 Date 类);或是类内成员为其他自定义类型(如 MyQueue 内含 Stack 成员),默认重载会自动调用内部类型的赋值逻辑。
  2. 必须手动实现:类内存在指针指向堆内存资源(如 Stack 类),默认浅拷贝只会复制指针地址,多个对象共享同一块堆空间,析构时重复释放内存会造成程序崩溃,需要手写深拷贝单独分配资源。

快速判断小技巧:只要一个类手动编写了析构函数用于释放堆资源,就必须配套手写拷贝构造和赋值运算符重载;无动态资源申请与释放的类,直接使用编译器默认生成的版本即可。

五、实现一个日期类Date

为了实践以上知识,我们着手实现一个日期类。

首先,定义出一个日期类以及我们想要实现的运算符重载函数。由于我们在实现这个类的过程中肯定会经常使用打印日期和获取某一月份总天数的功能,就先把它们实现一下:

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1);
	void Print();
	int GetMonthDay(int year, int month)
	{
		assert(month >0 && month < 13);
		static int monthDayArray[13] = { -1, 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);
	Date operator-=(int day);
	Date operator+(int dat);
	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);

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

5.1 加减运算符重载的实现

首先实现+=运算符的重载。+=是在当前对象的成员变量上直接加,不需要拷贝操作。这里的+=是一个进位逻辑:先让_day++,如果_day的值超过了当前月份的总天数,就进一位(让_month++),然后判断_month是否到了13,如果_month到了13就再进一位(让_year++),一直重复这个逻辑到_day的值合法为止。

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

	return *this;
}

+的运算符重载可以利用刚才已经写好的+=来实现:用拷贝构造来构造一个对象tmp,然后对它用+=,返回这个tmp就好了。(此处不能返回引用,因为tmp是一个临时变量,一旦返回引用就成了野引用)

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

而-=运算符的重载则是“借位”的逻辑:先让_day减去指定天数,如果减到零或负数,就进入循环:

此处循环中要先处理月份的逻辑:先让月份--,如果减到零就让年份--,月份回到12月;然后再让天数加上这个月份的总天数(借位),重复循环一直到天数合法。

Date Date::operator-=(int 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;
	tmp -= day;
	return tmp;
}

5.2 比较运算符重载的实现

先实现>运算符的重载:

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

其他比较运算符重载的实现,都可以复用写好的这两个:

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

5.3 单目运算符的重载实现
注意区分前置和后置的区别

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

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


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

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

5.3 日期相减

复用之前写过的函数,先比较一下两个日期,让小的那个日期不断++,一直加到与大的日期相等,算一下中间加了多少次。

int Date::operator- (const Date& d)
{
	int flag = 1;
	Date max = *this;
	Date min = d;

	if (min > max)
	{
		max = d;
		min = *this;
		flag = -1;
	}

	int n = 0;
	while (min != max)
	{
		++min;
		++n;
	}

	return n * flag;
}

5.4 流运算符重载

为了符合日常书写逻辑,要把流运算符重载为全局函数。

流输入运算符要加入一个处理非法输入的逻辑。

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

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

END

C++中的“类和对象” 类的定义,实例化,this指针 阅读详情

相关推荐

C++类和对象(上)

/类体:由成员函数和成员变量组成//一定要注意后面的分号class为定义类的关键字,ClassName为类的名字(自定义),{}中为类的主体,注意类定义结束时后面的分号不能省略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的方法或者成员函数。类的两种定义方式:1.声明和定义全部放在类体中,需注意: 成员函数如果在类中定义,编译器可能会将其当成内联函数处理。2.类声明放在.h文件中,成员函数的定义放在.cpp文件中。

m0_62183318的博客 807

【c++】cpp类和对象

c++中类和对象基本用法

hutaotaotao的博客 1268

用人话讲明白线性回归LinearRegression

目录 什么是回归 一元线性回归 损失函数 最小二乘估计 小结 1. 什么是回归 当我们学习一门新课程、接触一个新专业时,总会对该领域的专有名词感到困惑,甚至看完解释仍难以理解其含义。在我们一起学习machine learning的过程中,我会尽量对相关名词用“人话”做一遍解释,以减少学习的“痛苦感”。 譬如今天要学的线性“回归”,这个回归(regression)和我们平时说的“回归祖国”的回...

化简可得 1万+

Cpp-类和对象

title: 类和对象 date: 2019-03-15 14:05:24 tags: Cpp categories: Cpp 类的引入 万物皆对象。类是一种用户自定义的数据类型,包括表示属性的成员变量和表示行为的成员函数,类是现实世界对象的抽象,对象是类虚拟世界的实例。 C++ 中 struct 中不仅可以有变量,还可以有函数。 类定义结束时后面分号。 成员函数声明和定义可以全部...

ahojcn 的博客 1896

腾讯、飞书等在线表格自动化编辑--python

在线文档自动化处理数据

qq_32828053的博客 2万+

GB28181协议实战:5分钟搞定海康摄像头SIP注册失败问题(附Wireshark抓包分析)

本文详细解析了海康摄像头在GB28181协议下SIP注册失败的7个关键原因及解决方案,包括网络可达性、鉴权参数、NAT穿越等核心问题。通过Wireshark抓包分析,提供快速定位方法,帮助工程师高效解决SIP注册失败问题,提升实时图像联调效率。

weixin_29212867的博客 550

CPP:类和对象

cpp类和对象知识点

weixin_54832720的博客 969

【unity小技巧】新输入系统InputSystem游戏运行中改键重新绑定控制按键(最全最完美解决方案)

在游戏开发中,控制输入是至关重要的一环。Unity的新输入系统(Input System)为开发者提供了强大的工具来管理和处理用户输入,不仅支持各种平台上的多种输入设备,还能轻松处理复杂的输入操作和映射。本文将深入探讨如何使用Unity的新输入系统重新绑定控制按键,为开发者提供一套全面且完善的解决方案。

向宇的博客,专注php/web全栈 unity游戏开发,欢迎大家评论纠错 7356

cpp.1 C++类和对象(上)

文章目录第一部分 C++入门1.命名空间2.缺省参数3.函数重载4.引用 第一部分 C++入门 1.命名空间 命名空间实际上就是为了防止命名相同会导致很多的冲突,可以使用命名空间对标识符进行本地化,来避免命名的冲突 使用方式: 1.加命名空间名称及作用域限定符std::cout<<"hello world"<<std::endl; 2.使用using将命名空间中成员引入using std::cout; 3.用using namespace 命名空间名称引入using namespac

Pinksatrfish的博客 368

分立器件---电感介绍(7000字)

电感基础整理

HW-BASE的博客 2350

数据分片的三种方式:哈希、一致性哈希、按照数据范围分片

数据分片就是按照一定的规则,将数据集划分成相互独立、正交的数据子集,然后将数据子集分布到不同的节点上。通过设计合理的数据分片规则,可将系统中的数据分布在不同的物理数据库中,达到提升应用系统数据处理速度的目的。三种常用的数据分片方式:1. 哈希分片2. 一致性哈希分片(Consistent Hash)3. 按照数据范围分片(Range Based)数据分片的应用场景:1. 数据库扩展2. 分布式缓存对集群部署多个缓存节点,提高缓存的处理能力。

wjianwei666的专栏 1133

【Cpp】类和对象

类的认识,类和对象(一)

石墨博客 1173

详解大端模式和小端模式

详解大端模式和小端模式 一、大端模式和小端模式的起源         关于大端小端名词的由来,有一个有趣的故事,来自于Jonathan Swift的《格利佛游记》:Lilliput和Blefuscu这两个强国在过去的36个月中一直在苦战。战争的原因:大家都知道,吃鸡蛋的时候,原始的方法是打破鸡蛋较大的一端,可以那时的皇帝的祖父由于小时侯吃鸡蛋,按这种方法把手指弄破了,因此他的父亲,就下令,命令

ce123的技术博客 19万+

【C++】类和对象

类的基本思想是数据抽象(data abstraction)和封装(encapsulation)。数据抽象是一种依赖于接口(interface)和实现(implementatino)分离的编程技术。对象是类的实例,类是对象的模板。

jmlangel_的博客 5955

逆向工具:一文讲述 Ghidra 的详细使用

Ghidra是由NSA开源的一款功能强大的逆向工程框架,支持多平台、多架构和多种文件格式分析。本文详细介绍了Ghidra的核心优势、安装配置方法、主界面功能模块,并通过一个CrackMe示例演示了完整的逆向分析流程。文章还讲解了高级功能如交叉引用、控制流图分析、数据类型重建和脚本自动化等,最后提供了常见文件格式的分析技巧。作为IDA Pro的免费替代方案,Ghidra适合个人学习、教学和中小项目使用。(149字)

数据知道的博客 4337

深度学习工具包 DeepLearningToolbox

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

【C++指南】一文总结C++类和对象【下】

本章节主要讲初始化列表、类型转换、静态成员变量和函数、友元、内部类、匿名对象及编译器优化的各种情况。

egoist2023的博客 3493

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

【C++】类和对象核心总结

类的引入+定义(struct ==> class),类的访问限定符+封装+作用域,类的实例化(类 类型创建对象的过程)+计算类的大小(考虑内存对齐),隐藏的this指针,类的六个默认成员函数,初始化列表(声明远远不够,还得定义对象),explict关键字,static成员,友元,内部类,匿名对象,编译器优化

小羊study_sheep的博客 5592

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

【C++】类和对象——下

尽量使⽤初始化列表初始化,因为那些你不在初始化列表初始化的成员也会⾛初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会⽤这个缺省值初始化。内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使⽤,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其。友元提供了⼀种突破类访问限定符封装的⽅式,友元分为:友元函数和友元类,在函数声明或者类声明的前⾯加friend,并且把友元声明放到⼀个类的⾥⾯。

L286594的博客 1361

C++实验四——类和对象(2)

实验报告题目1题目2 【实验名称】 实验四 类和对象(2) 【实验内容】 题目1 课本习题4.10 设计一个用于人事管理的“人员”类。由于考虑到通用性,这里只抽象出所有类型人员都具有的属性:编号、性别、出生日期、身份证号等。其中“出生日期”声明为一个“日期”类内嵌子对象。用成员函数实现对人员信息的录人和显示。要求包括:构造函数和析构函数、复制构造函数、内联成员函数、带默认形参值的成员函数、类的组合。 源代码: Date.h /*出生日期类声明*/ #ifndef _Date_H_ #

Onlyone_1314的博客 2336

c++学习 类和对象上

【代码】c++学习 类和对象上。

qq_50934329的博客 197

【C++】类和对象(1)

C++类和对象,内存对齐,this指针,

Xie_sheng的博客 1791

【C++】类和对象【上】

本期博客带你了解C++中类和对象相关知识。

2401_86785052的博客 1042

C++ ——类和对象

C++ ——类和对象C++ ——类和对象C++ ——类和对象C++ ——类和对象C++ ——类和对象C++ ——类和对象

weixin_49376454的博客 1230

C++类和对象

声明和定义放到类体中public:public:char*_name;char*_sex;int _age;声明放.h文件中,类的定义放.cpp文件中。

mzk_c的博客 1822

cpp中的类和对象

类和对象的区别: 类(class):概念产物,是设计产物(相对于图纸),没有开辟空间 对象(object):是由类实例化出来的代码和数据的集合 类的实例化就成为了对象 我们操纵的是对象,不是类 封装特性:对外是不可访问的 this指针 编译过程: 第一步:识别类里面的属性成员(变量) 第二步:识别方法,只识别方法的声明 第三步:改写 ...

qq_44423388的博客 951
上一篇: C++基础
下一篇: C++ 类和对象(下)
jinyishu_
博客等级 码龄1年 53粉丝 82原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值