从零开学C++:类和对象(中)

news2024/9/25 15:29:33

引言:在我们学习了类和对象(上)的基础知识后,我们就需要进入类和对象(中)的学习。本篇博客将会介绍C++的几个默认成员函数,它们的存在虽然难以理解,但也是C++如此简单实用的原因之一。相信在你看完这篇博客之后一定会对C++有新的认识!

更多有关C语言数据结构的知识详解可前往个人主页:计信猫

一,类的六个默认成员函数

        假如当我们定义一个如下的空类

class Date {};

         那么此时这个里边真的什么都没有吗?答案是否定的,不管这个里边是否有任何成员,编译器在编译时都会生成六个默认成员函数,他们的作用如下图所示:

        其中默认成员函数的定义是用户不显式实现,编译器会自动生成的成员函数

二,构造函数

1,概念

        我们先来看下面一段代码:

class Date
{
public:
	//初始化函数
	void Init(int year, int month, int date)
	{
		_year = year;
		_month = month;
		_date = date;
	}
	int _year;
	int _month;
	int _date;
};
int main()
{
	Date d1;
	//初始化d1对象
	d1.Init(2024, 7, 11);
	return 0;
}

        在该段代码中,我们为Date类设计了一个成员类型的初始化函数当我们以这个类创建了一个对象d1时,就需要再调用一次Init函数为它初始化。那有没有什么方法可以让我们在创建对象的同时就将信息设置进去呢?

        当然有咯,答案就是我们的默认成员函数之一——构造函数构造函数是一个特殊的成员函数,它的作用就是初始化对象

2,特性

我们的构造函数有以下几条特性:

1,构造函数的名字和类名相同。

2,构造函数无返回值(就连void也不需要写)。

3,类的对象实例化时会自动被调用。

4,构造函数可以被重载。

        实例:

class Date
{
public:
	//构造函数,函数和类同名,并且没有返回值
	Date()
	{
		_year = 2024;
		_month = 7;
		_date = 11;
	}
	int _year;
	int _month;
	int _date;
};
int main()
{
	Date d1;
	return 0;
}

        此时,我们就定义了一个构造函数Date()成员类型进行初始化,当我们代码以走起来,那么这个构造函数就会被自动调用,此时我们观察d1的值就会发现d1中的三个成员类型都被自动初始化了。 

        当然,构造函数还有以下几点特性:

5,类中若无显式定义的构造函数,C++会自动生成一个无参数的构造函数。

6,无参、全缺省、编译器自动生成的构造函数,都被称作默认构造函数,但是三个函数不可以同时存在于类中。

7,编译器生成的构造函数对内置类型成员变量无要求。

        看下面的示例,都是一个合格的构造函数,并且附带有它们对应正确的对象创建方式:

//示例一
	Date()
{
	_year = 2024;
	_month = 7;
	_date = 11;
}
//于main中
Date d1;

//示例二
Date(int year, int month, int date)
{
	_year = year;
	_month = month;
	_date = date;
}
//于main中
Date d1(2024, 7, 11);

//实例三
Date(int year = 2024, int month = 7, int date = 11)
{
	_year = year;
	_month = month;
	_date = date;
}
//于main中
Date d1;

        当然,对于当类中存在其他类型并且其他类型已有对应的构造函数,那么此时我们就不需要自己再写构造函数了。例子如下:

class stack
{
	//stack已存在构造函数
};
class queue
{
private:
	stack pushst;
	stack popst;//此时编译器就会调用stack的构造函数,就不需要专门为queue创建构造函数了
};

3,小总结

        在大多数情况下,都需要我们自己写构造函数,养成一个好习惯,除了刚刚上面讲的情况以外,构造函数我们还是应写尽写吧。

三,析构函数

1,概念

        当我们创建完一个对象对象的生命周期结束之后,那么这个对象是怎样消失的呢?则此时就不得不提到一个和构造函数相对的函数——析构函数了。

        析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

2,特性

         我们的析构函数有以下几条特性:

1,析构函数的函数名是在类名之前加上“~”。

2,析构函数没有参数,也没有返回值。

3,若析构函数没有被显式定义,那么系统会自动生成。

4,当对象的生命周期结束时,系统会自动调用析构函数。

        那我们就自己来定义一个简单的析构函数吧!

class Stack
{
public:
	//构造函数
	Stack(int capacity=4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == NULL)
		{
			perror("malloc fail!");
			return;
		}
		_top = 0;
		_capacity = capacity;
	}
	//析构函数
	~Stack()
	{
		free(_a);
		_a = NULL;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

         此时,当类创建的对象生命周期为零时,那么系统就会自动调用这个析构函数对资源进行销毁

        当然,我们的析构函数还具有以下的性质:

5,如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。

        让我们再继续看一个小例子:

class Time
{
public:
	~Time()
	{
		cout << "~Time()" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

        当我们代码一走,会出现什么结果呢?

        由此结果我们可以发现,Time中的销毁函数被调用,但是在main()中我们并没有定义有关Time对象啊,这是为什么呢?

         其实答案很简单,因为我们创建了一个Date类对象d1,而d1当中存在着内置类型Time。当我们想要销毁d1时,则d1成员类型_year,_month,_date三个成员不需要销毁,编译器回收即可,但是Time类型成员_t却需要被销毁,而此时编译器就会自动生成一个析构函数,而这个析构函数就会调用Time类中的析构函数也就是~Time(),以此达到销毁_t变量的目的

 3,小总结

        所以我们现在可以知道,析构函数也就是一个帮助销毁变量的函数。当没有显式定义时编译器会自动生成。如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,而一旦有申请资源时,就最好还是自己将析构函数写出来,防止内存泄漏

四,拷贝构造函数

1,概念

        拷贝构造函数其实是特殊的构造函数。它的第一个参数为自身类型的引用,并且额外参数都有对应的缺省值

2,特性

        那么我们就像来写一段拷贝构造函数并且试着在main函数中使用。

class Date
{
public:
	//构造函数
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//拷贝构造函数
	Date(const Date& d) 
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	//使用拷贝构造函数
	//写法一:
	Date d2(d1);
	//写法二:
	Date d2 = d1;
	return 0;
}

        此时我们代码一走,我们就会发现系统创建了d1d2两个变量,并且d2的值由d1拷贝而来

        那么我们的拷贝构造函数拥有着一下几点特性:

1,拷贝构造函数和构造函数构成函数重载。

2,自定义类型的传值传参和传值返回都由拷贝构造函数完成。

3,拷贝构造函数第一个参数必须为类型对象的引用,不然会因为造成的无限递归而报错。

4,若无显式定义拷贝构造函数,编译器会自动生成。但该函数只会对内置类型完成浅拷贝。

         当然,文字固然枯燥难懂,现在我将举例对其中几点特性进行说明:

Ⅰ,第二点,第三点

        其实第二点的含义很简单,我们看如下的一张图示:

        这张图就可以很好地解释第二点,当一个函数的参数里边含有自定义类型的时候,那么此时编译器就会调用拷贝构造函数,将该对象的数据通过拷贝构造函数传递给该函数,再由该函数进行使用。同理,如果这个函数的返回值是自定义类型,此时返回值也会通过拷贝构造函数传递给接收值。 

        有了这个知识做准备,我们就可以对第三点进行讲解了。那么我们就先设计一个第一个参数不为类类型对象的引用的例子,看看会发生什么状况。

        如图所示,如果不使用对象的引用的话,那么编译器会一直调用拷贝构造函数,无限递归造成错误。 

Ⅱ,第四点

        在要搞懂第四点之前,我们就需要首先搞懂浅拷贝深拷贝分别的含义。浅拷贝就是拷贝构造函数对象按内存存储按字节序完成拷贝,也就是差不多一个一个字节挨着拷贝,适合于没有空间资源申请的时候的拷贝,而深拷贝就是在浅拷贝的基础上,还将申请的资源空间等全部内容一并拷贝。        

        所以,当我们没有显式定义一个拷贝构造函数时,编译器会自动生成一个默认的拷贝构造函数,但这个函数只能进行浅拷贝。

推导:像Date类型中,全为内置类型,就可以不用写拷贝构造函数,但类似Stack类,有空间资源的申请时,我们就需要自己写一个拷贝构造函数来完成深拷贝。

        那下面我们就来写一个Stack类的拷贝构造函数吧!

class Stack
{
public:
	//拷贝构造函数
	Stack(const Stack& s1)
	{
		_a = (int*)malloc(sizeof(int) * s1._capacity);
		if (_a == NULL)
		{
			perror("malloc fail!");
			return;
		}
		memcpy(_a, s1._a, sizeof(int) * s1._top);
		_top = s1._top;
		_capacity = s1._capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

 3,小提醒

        让我们来看下面两段代码,观察他们是否有问题:

//代码一:
Stack func1()
{
	Stack st;
	return st;
}
//代码二:
Stack& func1()
{
	Stack st;
	return st;
}

        那么答案很简单,第一段代码是正确的,而第二段代码存在错误。在第二段代码中,Stack&返回值,表示这个函数返回的是st变量的别名,但是一旦我们出了这个函数,那么这个st就会被系统自动删除,那么此时就相当于指针知识里边的野指针了。

        但是想改正也很简单,只需要像如下改正就可以了:

//代码二:
Stack& func1()
{
	static Stack st;
	return st;
}

4,小总结

        若一个显式实现了析构函数并且析构函数中释放了资源,那么这个也需要写一个拷贝构造函数了。

五,赋值运算符重载

1,引入

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

        所以,运算符重载代表的是一类函数,由C++关键字operator运算符构成,针对于自定义类型。格式如下:

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

        那么此时就让我们来试着构造一个比较两个类是否相等的运算符重载函数吧。 

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

         此时这个函数就在Date类中构造好了,于是我们便可以如下使用这个函数:

int main()
{
	Date d1;
	Date d2(d1);
	if (d1 == d2)//或者写为d1.operator==(d2)
	{
		cout << "两个对象相等" << endl;
	}
	return 0;
}

        此时代码走起来,那么结果如下,说明运算符重载函数设计成功!

2,特性

        当然,我们的运算符重载函数具有以下几点特性:

1,不能通过连接其他符号来创建新的操作符:比如operator@。

2,重载操作符必须有一个类类型参数。

3,用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义。

4,作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this。(函数参数按从左往右的顺序依次放入操作符需要值的地方)

5,.* 、::、 sizeof 、?:、 . 注意以上5个运算符不能重载。  

3,小例子

        那我们现在来试着实现一个用于Date类的对象赋值的运算符重载函数吧!

//赋值运算符重载
void operator=(Date d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}
int main()
{
    //调用构造函数
	Date d1(1999,10,1);
    //调用拷贝构造函数
	Date d2;
	d2.Print();
	return 0;
}

        此时我们实现了该函数,在main中代码一走,结果如下,说明实现成功。 

        但是现在就出现了一个小问题,该函数无法进行连续赋值的操作,并且函数的参数设计得不完美,但我们可以对其进行如下改变:

//赋值运算符重载
Date& operator=(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

        此时无法连续赋值的问题就被解决了,如下图所示:

        而此时我们将函数的返回值和参数都设置为类的对象的引用,那么就省去了调用拷贝构造函数的那一步,这时候程序的效率再一次被提高。

4,小总结

        若一个显式实现了析构函数时,就需要显式实现赋值运算符重载函数

六,取地址及const取地址运算符重载

1,const

        其实在之前我们就已经学到过const修饰的变量会改变该变量的权限,并且权限只可以缩小,不可以扩大。那么在这个雷打不动的规定之下,我们定义的重载函数的参数和返回值就有了一定的要求

        我们首先看一个Date类成员函数的声明:

void Print();

        然后我们将这个函数隐藏的this指针也写出来会发现其实这个函数是如下这样的:

//打印函数
void Print(Date* const this);

         在使用这个函数的时候,如果我们类的对象的类型为Date时,还可以使用,可一旦我们的变量类型为const Date时,那么此时再使用该函数,就会造成权限的放大问题而无法使用,所以我们需要对该函数的参数做一些改变,如下:

//打印函数
void Print(const Date* const this) => void Print()const;

        那么此时这个函数就可以适用于const Date类型的对象了。

2,const取地址和取地址

        那么有了上面的知识的辅助,学这里的知识点就非常简单了。这两个函数其实不用自己显式定义,因为编译器会自动生成

//取地址运算符重载
Date* operator&()
{
	return this;
}
//const取地址运算符重载
const Date* operator&()const
{
	return this;
}

        这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!

七,结语

        现在我们学习到的知识都是在为了之后学习C++的起飞知识做的铺垫这个章节可能只是比较多比较难,但是相信我们只要认真学习,仔细理解,还是很容易听懂的,有什么问题欢迎找我沟通或者在评论区讨论。

        接下来我将更新类和对象(下)的知识点,困难的知识都差不多在本章节讲完了,下篇博客也就是收尾知识了,加油学习!相信我们的能力会在这个暑假突飞猛进!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1924009.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++学习指南(一)——C++入门基础

欢迎来到繁星的CSDN&#xff0c;本期内容主要包括C第一个程序&#xff0c;命名空间&#xff0c;缺省参数&#xff0c;函数重载&#xff0c;引用、inline以及nullptr这些基础概念。 在进入正题之前&#xff0c;我需要先阐述一下。本系列涉及的内容为C部分&#xff0c;可以理解为…

The Open Group 爱丁堡大会高光集锦——企业架构、人工智能和可持续发展的创新交叉点

4月底&#xff0c;The Open Group峰会在英国爱丁堡顺利举办。活动邀请到数十位领域专家、技术、论坛成员、工作组和联合组织等相聚在一起&#xff0c;围绕生态系统架构和人工智能标准、可持续性、企业架构、数字转型等话题进行了对话与探讨。大会吸引了来自30个国家的400位观众…

bi项目笔记

1.bi是什么 bi项目就是商业智能系统&#xff0c;也就是数据可视画、报表可视化系统&#xff0c;如下图的就是bi项目了 2.技术栈

Mysql数据库的备份与恢复以及索引操作

一&#xff0c;备份与恢复操作 1&#xff0c;创建数据库booksDB CREATE DATABASE booksDB; use booksDB; 2&#xff0c;建表 &#xff08;1&#xff09;创建表books CREATE TABLE books ( bk_id INT NOT NULL PRIMARY KEY, bk_title VARCHAR(50) NOT NUL…

MYSQL--第八次作业

MYSQL–第八次作业 一、备份与恢复 环境搭建&#xff1a; CREATE DATABASE booksDB; use booksDB;CREATE TABLE books ( bk_id INT NOT NULL PRIMARY KEY, bk_title VARCHAR(50) NOT NULL, copyright YEAR NOT NULL );CREATE TABLE authors ( auth_id INT NOT NULL PRI…

SpringCloud第三篇(服务中心与OpenFeign)

p 文章目录 一、服务中心二、Nacos注册中心 一、服务中心 在上一章我们实现了微服务拆分&#xff0c;并且通过Http请求实现了跨微服务的远程调用。不过这种手动发送Http请求的方式存在一些问题。 试想一下&#xff0c;假如商品微服务被调用较多&#xff0c;为了应对更高的并发…

【JavaEE】AOP实现原理

概述 Spring AOP 是基于动态代理来实现AOP的, 此处主要介绍代理模式和Spring AOP的源码剖析 一.代理模式 代理模式是一种常用的设计模式&#xff0c;它允许为其他对象提供代理&#xff0c;以控制对这个对象的访问。这种结构在不改变原始类的基础上&#xff0c;通过引入代理类…

前端的页面代码

根据老师教的前端页面的知识&#xff0c;加上我也是借鉴了老师上课所说的代码&#xff0c;马马虎虎的写出了页面。如下代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</ti…

Gitea 仓库事件触发Jenkins远程构建

文章目录 引言I Gitea 仓库事件触发Jenkins远程构建1.1 Jenkins配置1.2 Gitea 配置引言 应用场景:项目部署 I Gitea 仓库事件触发Jenkins远程构建 Gitea支持用于仓库事件的Webhooks 1.1 Jenkins配置 高版本Jenkins需要关闭跨域限制和开启匿名用户访问 在Jenkins启动前加入…

微前端基础知识

1. 前言 随着Web应用程序规模的日益扩大和复杂性的增加&#xff0c;传统的前端开发模式逐渐显现出其在维护、扩展以及团队协作方面的局限性。微前端作为一种新兴的前端架构模式&#xff0c;正是为了应对这些挑战而诞生的。 微前端&#xff08;Micro-Frontends&#xff09;并没有…

matine组件库踩坑日记 --- react

Mantine实践 一 禁忌核心css样式二 添加轮播图扩展组件 一 禁忌核心css样式 import React from react import ReactDOM from react-dom/client import { BrowserRouter } from react-router-dom; import App from ./App.jsx import ./index.css import mantine/core/styles.cs…

收银系统源码-会员功能

随着新零售时代不断更新迭代&#xff0c;私域会员已经成为很多连锁门店必要的选择。自然离开不了一套能高效管理会员的收银系统。今天给大家推荐一下&#xff0c;智慧新零售收银系统的会员功能。 了解更多查看下文&#xff1a; 门店收银系统源码-CSDN博客文章浏览阅读2.6k次&…

开源项目:机遇与挑战共存的创新之路

开源项目&#xff1a;机遇与挑战共存的创新之路 开源&#xff08;Open Source&#xff0c;开放源码&#xff09;被非盈利软件组织&#xff08;美国的Open Source Initiative协会&#xff09;注册为认证标记&#xff0c;并对其进行了正式的定义&#xff0c;用于描述那些源码可以…

倒计时 2 周!CommunityOverCode Asia 2024 IoT Community 专题部分

CommunityOverCode 是 Apache 软件基金会&#xff08;ASF&#xff09;的官方全球系列大会&#xff0c;其前身为 ApacheCon。自 1998 年以来&#xff0c;在 ASF 成立之前&#xff0c;ApacheCon 已经吸引了各个层次的参与者&#xff0c;在 300 多个 Apache 项目及其不同的社区中探…

线性回归(梯度下降)

首先说案例&#xff1a; 房子的价格和所占面积有着很大的关系&#xff0c;假如现在有一些关于房子面积和价格的数据&#xff0c;我要如何根据已经有的数据来判断未知的数据呢&#xff1f; 假如x(房屋面积)&#xff0c;y(房屋价格) x[ 56 72 69 88 102 86 76 79 94 74] y[92, …

struts2如何防止XSS脚本攻击(XSS防跨站脚本攻击过滤器)

只需要配置一个拦截器即可解决参数内容替换 一、配置web.xml <filter><filter-name>struts-xssFilter</filter-name><filter-class>*.*.filters.XssFilter</filter-class></filter><filter-mapping><filter-name>struts-xss…

1.5.1抽象java入门

前言&#xff1a; 1.5.0版本中&#xff0c;我们熟练使用Git三个可视化操作&#xff08;签出&#xff0c;提交&#xff0c;对比&#xff09;&#xff0c;再加上1.4.0版本的新建&#xff0c;总计使用四个Git可视化操作&#xff1b;对java编程的学习&#xff0c;总结&#xff0c;…

vector 介绍

1.简述vector 首先我们要大致弄明白vector是一个什么东西,其实vector就是之前我们学过的顺序表,这里直接使用就行了. 定义vector-------->vector<typename> arr 此时的这种定义vector可以理解成为一个数组,而typename可以是各种数据类型,比如string,int,double....…

react启用mobx @decorators装饰器语法

react如果没有经过配置&#xff0c;直接使用decorators装饰器语法会报错&#xff1a; Support for the experimental syntax ‘decorators’ isn’t currently enabled 因为react默认是不支持装饰器语法&#xff0c;需要做一些配置来启用装饰器语法。 step1: 在 tsconfig.js…

宪法学学习笔记(个人向) Part.6

宪法学学习笔记(个人向) Part.6 5. 国家机构 概述 国家机构是国家为了实现其管理社会、维护社会秩序职能而建立起来的国家机关的总和&#xff1b;它包括&#xff1a; 立法机关&#xff08;全国人大及其常委会&#xff09;;行政机关&#xff08;国务院和地方人民政府&#xff09…