《C++ primer plus》精炼(OOP部分)——对象和类(6)

news2024/12/23 18:48:12

“学习不是生活的一部分,而是构建生活的全部。”——约翰·杜

文章目录

  • 第12章:类和动态内存分配
    • 第1版:Stringbad类
      • 错误
      • 小知识点:new和delete的对应使用
    • 第二版:String类
    • 构造函数中使用new时的注意事项

第12章:类和动态内存分配

在这一章中,我们逐步构造了一个自己的简易版String类,这个类在构造和析构函数中频繁使用new和delete运算符,因此可以引出一些在类中使用动态内存分配所隐含的问题。

第1版:Stringbad类

Stringbad类使用动态内存分配完成了构造和析构操作,但也引发了一些问题。下面是Stringbad类声明:

#include<iostream>
#ifndef STRINGBAD_H_
#define STRINGBAD_H_

class StringBad
{
private:
	char* str;//pointer to string
	int len;
	static int num_strings;//number of objects
public:
	StringBad(const char* s);
	StringBad();
	~StringBad();
	friend std::ostream& operator<<(std::ostream& os, const StringBad& st);
};

#endif

注意,在声明中加入了一个静态成员变量,这种变量独立于所有对象存储,被所有对象所共享,它表示已经创建的对象的个数。
以下是StringBad类的定义:

#include<cstring>
#include "stringbad.h"
using namespace std;
//初始化静态类成员
int StringBad::num_strings = 0;
//类方法
StringBad::StringBad(const char* s)
{
	len = strlen(s);
	str = new char[len + 1];
	strcpy(str, s);
	num_strings++;
	cout << num_strings << ":\"" << str << "\"object created\n";
}

StringBad::StringBad()
{
	len = 4;
	str = new char[4];
	strcpy(str, "C++");
	num_strings++;
	cout << num_strings << ":\"" << str << "\"object created\n";
}

StringBad::~StringBad()
{
	cout << "\"" << str << "\" object deleted, ";
	--num_strings;
	cout << num_strings << "left\n";
	delete[] str;
}

std::ostream& operator<<(std::ostream& os, const StringBad& st)
{
	os << st.str;
	return os;
}
  1. 在每个构造函数中,递增num_strings;每个析构函数中递减num_strings
  2. 以传入参数的构造函数为例,先用字符串s初始化len和str,初始化str时使用new运算符分配了足够存储字符串的内存。然后用strcpy函数进行字符串拷贝,递增num_strings,输出字符串信息。注意,不能直接用“str=s”,这样相当于拷贝了地址,而非字符串本身。
  3. 对于析构函数,则先输出要析构的字符串信息,递减num_strings,最后调用delete运算符进行内存释放。注意,当对象被析构时,str指针会被自动释放,但分配的内存不会,因此会造成内存泄露,所以在C++中要注意一个new对应一个delete,这点和java不同。

以下是对StringBad类的测试程序:

#include<iostream>
using std::cout;
#include"stringbad.h"

void callme1(StringBad&);//引用传参
void callme2(StringBad);//值传参

int main(void)
{
	using std::endl;
	{
		cout << "Starting an inner block.\n";
		StringBad headline1("Celery Stalks at Midnight");
		StringBad headline2("Letture Prey");
		StringBad sports("Spinach Leaves Bowl for Dollars");

		cout << "headline1: " << headline1 << endl;
		cout << "headline2: " << headline2 << endl;
		cout << "sports: " << sports << endl;
		callme1(headline1);
		cout << "headline1: " << headline1 << endl;
		callme2(headline2);
		cout << "headline2: " << headline2 << endl;
		cout << "Initialize one object to another:\n";
		StringBad sailor = sports;
		cout << "sailor: " << sailor << endl;
		cout << "Assign one object to another:\n";
		StringBad knot;
		knot = headline1;
		cout << "knot: " << knot << endl;
		cout << "Exiting the block.\n";
	}
	cout << "End of main()\n";
	return 0;
}

void callme1(StringBad& rsb)
{
	cout << "String passed by reference:\n";
	cout << "   \"" << rsb << "\"\n";
}

void callme2(StringBad sb)
{
	cout << "String passed by value:\n";
	cout << "   \"" << sb << "\"\n";
}

输出结果:
在这里插入图片描述
在这里插入图片描述
注意最后几行的输出信息,很明显出现了问题,初步判断是在调用析构函数时字符串的内容出了问题,而且num_strings最后的值是-2,也不对。
哪里出错了?

错误

StringBad类的错误是由特殊成员函数引起的。如果编译器判断程序使用对象时需要这些函数且程序员没有写,C++就会自动提供。C++中的特殊成员函数有:

  1. 默认构造函数
  2. 默认析构函数
  3. 复制构造函数
  4. 赋值运算符
  5. 地址运算符

这个程序的问题就在于编译器自动提供了复制构造函数和赋值运算符,但提供的不对。
注意下面的两段节选代码:

  1. 第一段
callme2(headline2)

对于这条语句,调用了callme2函数,以值传参的方式传入了headline2。我们都知道,值传参时隐式调用复制构造函数构造一个临时对象。但编译器自动提供的复制构造函数显然会是这样的:

StringBad(StringBad s)
{
	str=s.str;
	len=s.len;
}

这会导致两个问题:

  1. num_strings没有递增,但临时对象在析构时也要调用构造函数,会造成num_strings递减。
  2. 直接将s的字符串指针地址传给临时对象,这样的话临时对象析构时会将s的字符串释放掉,使其无法识别。

我们称编译器自动提供的拷贝构造函数为浅拷贝,只复制了字符串地址而没有复制字符串本身,因此我们需要提供一个深拷贝的构造函数:

StringBad(StringBad s)
{
	num_strings++;//解决第一个问题
	len=s.len;
	str=new char[len+1];
	std::strcpy(str,st.str);//进行字符串本身的复制,解决第二个问题
	cout<<num_strings<<":\""<<str<<"\" object created\n";
}
  1. 第二段
StringBad sailor = sports;
knot = headline1;

这两条语句调用了编译器自动提供的赋值运算符重载函数。第一条语句可能有两种实现,第一种是直接进行赋值运算符;第二种是先拷贝构造一个sports的临时对象,然后再使用赋值运算符,这两种解决方式显然都会出问题,因为都调用了错误的函数。
编译器自动提供的赋值运算符重载显然应该是这样:

StringBad operator=(StringBad s)
{
	str=s.str;
	len=s.len;
	return *this;//返回被赋值的类对象
}

这犯了同一个毛病,直接将字符串的地址进行赋值,而没有复制整个字符串进行赋值。注意,赋值运算符并没有新增一个StringBad类对象,不需要递增num_strings。
需要提供一个自己写的赋值运算符重载:

StringBad operator=(StringBad s)
{
	len=s.len;
	str=new char[len+1];
	std::strcpy(str,st.str);//进行字符串本身的复制
	return *this;//返回被赋值的类对象
}

将这两个函数加入后,StringBad类就可以正常使用。

小知识点:new和delete的对应使用

在使用new运算符时,有两种方法:

str=new char;
str=new char[1];

这两种方法分配的内存量一样,区别在于前者要用delete运算符释放内存,后者用delete[]运算符释放内存。注意,任何隐式使用new运算符的方法都调用new而非new[]。

delete str;//兼容new运算符
delete[] str//兼容new[]运算符

delete[]运算符也兼容空指针,因此以下的代码能通过编译:

str=0;//将str置空
delete[] str;

第二版:String类

解决了这两个问题后,我们可以完善String类。下面是String类的头文件:

#include <iostream>
#ifndef STRING_H_
#define STRING_H_
class String
{
private:
	char* str;//对string的指针
	int len;//string的长度
	static int num_strings;//字符串的数量,由变量由所有对象共享
	static const int CINLIM = 80;//设置输入缓冲区限制
public:
	String(const char* s);
	String();
	String(const String& st);
	~String();
	int length()const//返回被存储的字符串的长度
	{
		return len;
	}
	//下面三个友元函数对字符串进行比较
	friend bool operator<(const String& st, const String& st2);
	friend bool operator>(const String& st1,const String& st2);
	friend bool operator==(const String& st, const String& st2);
	//提供简单的输入功能
	friend std::istream& operator>>(std::istream& is, String& st);
	//下面两个函数提供用中括号访问字符串中各个字符的功能
	char& operator[](int i);
	const char& operator[](int i)const;
	static int HowMany();//展示静态类数据成员num_string
	friend std::ostream& operator<<(std::ostream& os, const String& st);
	String& operator=(const String& st);
	String& operator=(const char*);
};
#endif
  1. 仿照标准程序库中的String类重载了>,<,==三个运算符,以字典序比较两个字符串。
  2. 也是仿照String类重载了[]运算符,通过[]运算符能访问字符串中特定的元素。分别重载const版本和普通版本是为了访问const字符串,如果没有声明为const的函数,用[]运算符访问const字符串时将出错:
cout<<const_string[1];//如果这个类对象是const类型的,那么就必须用const版本的函数
  1. 注意这一个函数:
static int HowMany();//展示静态类数据成员num_string

这是一个和num_strings相似的静态函数,这个函数只能由类调用,不能通过对象调用,也因为这个原因它只能调用类的静态数据成员:

String::HowMany();//通过String类直接调用
  1. 增加参数为const char*的赋值运算符重载,使得程序能直接赋值=运算符给String对象,而不用先构造出临时对象再赋值。
  2. 静态数据成员CINLIM表示对输入(cin)的限制(limit),具体做法看实现。

以下是String类的实现:

#define _CRT_SECURE_NO_WARNINGS 1
#include<cstring>
#include "string.h"
using namespace std;
//初始化静态类成员
int String::num_strings = 0;
//类方法,从C的String构造String
String::String(const char* s)
{
	len = strlen(s);//设置长度
	str = new char[len + 1];//分配存储空间
	strcpy(str, s);//初始化指针
	num_strings++;//设置字符串数量
}

String::String()
{
	len = 0;
	str = new char[1];
	str[0] = '\0';
	num_strings++;
}

String::String(const String& st)
{
	num_strings++;//设置静态变量
	len = st.len;//设置相同长度
	str = new char[len + 1];//开辟内存
	strcpy(str, st.str);//将字符串复制到新的地址
}

String::~String()
{
	--num_strings;//required
	delete[]str;//required
}

bool operator<(const String& st1, const String& st2)
{
	return(strcmp(st1.str, st2.str) < 0);
}

bool operator>(const String& st1, const String& st2)
{
	return st2 < st1;
}

bool operator==(const String& st1, const String& st2)
{
	return(strcmp(st1.str, st2.str) == 0);
}

istream& operator>>(istream& is, String& st)
{
	char temp[String::CINLIM];
	is.get(temp, String::CINLIM);
	if (is)
		st = temp;
	while (is && is.get() != '\n')
		continue;
	return is;
}

char& String::operator[](int i)
{
	return str[i];
}

const char& String::operator[](int i)const
{
	return str[i];
}

int String::HowMany()
{
	return num_strings;
}

ostream& operator<<(ostream& os, const String& st)
{
	os << st.str;
	return os;
}

String& String::operator=(const String& st)
{
	if (this == &st)//对象指向自己
		return *this;//如果不额外声明一个if,那么给对象重新赋值时,释放内存操作可能删除对象的内容
	delete[]str;//释放旧的字符串,以将新的字符串复制到地址
	//进行新的赋值操作
	len = st.len;
	str = new char[len + 1];//为新字符串开辟内存空间
	strcpy(str, st.str);
	return *this;//返回对调用对象的引用
}

String& String::operator=(const char* s)
{
	delete[]str;
	len = strlen(s);
	str = new char[len + 1];
	strcpy(str, s);
	return *this;
}

其他的看一看也就行了,自己写都能写出来,主要是>>运算符的重载,有一个get函数:

is.get(temp, String::CINLIM);

get函数读取CINLIM个字符到字符串temp中。读取失败则将is置为false。

构造函数中使用new时的注意事项

  1. 构造函数中使用new,则析构函数中需要使用delete
  2. new对应delete,new[]对应delete[]
  3. 多个构造函数必须用同一种方式使用new,即要么都用new,要么都用new[],因为析构函数只能有一个,其中要么用delete,要么用delete[]。
  4. 当判断浅拷贝和浅赋值不够用时,需要自定义深度拷贝和深度赋值运算符重载函数。

请添加图片描述
我是霜_哀,在算法之路上努力前行的一位萌新,感谢你的阅读!如果觉得好的话,可以关注一下,我会在将来带来更多更全面的知识讲解!

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

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

相关文章

【校招VIP】java语言考点之嵌套类内部类

考点介绍&#xff1a; 嵌套类&内部类问题在校招面试中经常出现。以在一个类的内部定义另一个类&#xff0c;这种类称为嵌套类(nested classes)&#xff0c;它有两种类型: 静态嵌套类和非静态嵌套类。静态嵌套类使用很少最重要的是非静态嵌套类&#xff0c;也即是被称作为内…

spring-2.5.6升级为spring-4.3.13过程记录

一、首先不管三七二十一&#xff0c;把spring-2.5.6的包全删除了 引进spring-4.3.13的包 二、参考https://blog.csdn.net/weixin_33978016/article/details/92103733修改 web.xml修改&#xff0c;我原项目跟他一样&#xff0c;所以不用改 修改前 <servlet><servlet…

Spring之bean的生命周期源码解析

Spring最重要的功能就是帮助程序员创建对象&#xff08;也就是IOC&#xff09;&#xff0c;而启动Spring就是为创建Bean对象做准备&#xff0c;所以我们先明白Spring到底是怎么去创建Bean的&#xff0c;也就是先弄明白Bean的生命周期。 Bean的生命周期就是指&#xff1a;在Spr…

AI创作专家,免费的AI创作专家工具

AI创作专家是一种崭新的工具&#xff0c;它们利用先进的人工智能技术&#xff0c;帮助创作者和写手更轻松地应对创作挑战。这些工具不仅可以生成文字&#xff0c;还可以提供灵感、帮助构思和组织思路&#xff0c;使创作过程更加高效。 147GPT批量文章生成工具​www.147seo.com/…

【python】ray库使用

【python】ray库使用 安装案例运行案例代码&#xff08;torch&#xff09;运行输出解释案例代码&#xff08;tensorflow&#xff09;运行结果 安装 注意事项&#xff1a; 在windows下&#xff0c;需要python版本3.7以上&#xff0c;详见https://docs.ray.io/en/latest/ray-ove…

DT 卡通材质学习 一

渐变着色器 相交线 笔刷和卡通结合使用 修改器

停车场系统源码

源码下载地址&#xff08;小程序开源地址&#xff09;&#xff1a;停车场系统小程序&#xff0c;新能源电动车充电系统&#xff0c;智慧社区物业人脸门禁小程序: 【涵盖内容】&#xff1a;城市智慧停车系统&#xff0c;汽车新能源充电&#xff0c;两轮电动车充电&#xff0c;物…

VSCode远程连接服务器报错:Could not establish connection to

参考&#xff1a;https://blog.csdn.net/weixin_42538848/article/details/118113262 https://www.jb51.net/article/219138.htm 刚开始把ssh文件夹中的known_hosts给删除了&#xff0c;发现没啥用。 之后在扩展Remote-SSH里面&#xff0c;把config file路径设置为ssh文件夹里…

壁炉的智能化:现代设计师的创新挑战

壁炉一直以来都是家庭的焦点之一&#xff0c;不仅因为它们提供了温暖&#xff0c;更因为它们在室内空间中的装饰价值。然而&#xff0c;如今的壁炉不再仅仅是传统的取暖设备&#xff0c;它们变得更加智能化&#xff0c;提供了更多的功能和便利性。对于室内设计师来说&#xff0…

Nginx图片防盗链

原理 浏览器向web服务器发送请求时一般会在header中带上Referer信息&#xff0c;服务器可以借此获得一些信息用来处理盗链 不过Referer头信息其实是可以伪装生成的&#xff0c;所以通过Referer信息防盗链并非100%可靠 具体方法 核心点就是在Nginx配置文件中&#xff0c;加入…

我的Qt作品(19)使用Qt写一个轻量级的视觉框架---第2章,仿海康VM实现思维导图拖拽方式的算法流程图

上次写的第1章介绍了主界面的设计。 https://blog.csdn.net/libaineu2004/article/details/130277151 本次是第2章&#xff0c;主要介绍流程图的运行。 目前市面上视觉框架很多&#xff0c;主要有列表图方式和流程图方式。海康VM的流程图方式比较受用户的喜爱和欢迎&#xf…

记一次失败的pip使用经历

python如何使用pip工具下载第三方库&#xff1f; 首先&#xff0c;安装并配置好python和pip的环境&#xff0c;特别注意pip放在python的script文件下&#xff0c;有pip和pip3两种&#xff0c;选择pip3版本。如下图所示。 然后打开命令行窗口&#xff0c;检查python和pip工具是…

iterator和generator

iterator和generator iterator es6: let/const ...展开 迭代器 是一种机制&#xff0c;比如在控制台输出Iterator是没有这个类的&#xff0c;为不同的数据结构提供迭代循环的机制。 迭代器对象&#xff1a;具备next方法&#xff0c;next能够对你指定的数据进行迭代循环&#x…

Vue 的组件加载顺序和渲染顺序

1、结论先行 组件的加载顺序是自上而下的&#xff0c;也就是先加载父组件&#xff0c;再递归地加载其所有的子组件。 而组件渲染顺序是按照深度优先遍历的方式&#xff0c;也就是先渲染最深层的子组件&#xff0c;再依次向上渲染其父组件。 2、案例 下面是一个简单的示例代…

灰度变换 几种常见的空间滤波,例如均值、中值滤波(数字图像处理概念 P3)

文章目录 背景知识 & 一些基础的变换直方图处理 ★均值滤波器中值滤波器锐化空间滤波器 增强的首要目标是处理图像&#xff0c;使其更适合某些应用 图像质量的视觉评价是一种高度主观的过程 背景知识 & 一些基础的变换 直方图处理 ★ 均值滤波器 中值滤波器 锐化空间滤…

接口自动化测试之Requests模块详解

Python中&#xff0c;系统自带的urllib和urllib2都提供了功能强大的HTTP支持&#xff0c;但是API接口确实太难用了。Requests 作为更高一层的封装&#xff0c;在大部分情况下对得起它的slogan——HTTP for Humans。 让我们一起来看看 Requests 这个 HTTP库在我们接口自动化测试…

关键点检测 HRNet网络详解笔记

关键点检测 HRNet网络详解笔记 0、COCO数据集百度云下载地址1、背景介绍2、HRNet网络结构3、预测结果&#xff08;heatmap&#xff09;的可视化3、COCO数据集中标注的17个关键点4、损失的计算5、评价准则6、数据增强7、模型训练 论文名称&#xff1a; Deep High-Resolution Rep…

Parasoft Jtest 2023.1

Parasoft Jtest 2023.1 2692407267qq.com&#xff0c;更多内容请见http://user.qzone.qq.com/2692407267/

知识图谱:信息抽取简易流程

目录 一、标注训练数据 二、训练数据模型 三、实现NER 一、标注训练数据 使用工具:Brat ## BRAT安装 0、安装条件 (1)运行于Linux系统 (2)brat(v1.3p1)仅支持python2版本运行使用,否则会报错 File "standalone.py", line 257except SystemExit, sts:^Syn…

探索最佳建筑工程项目管理软件,提高效率与协作

相比于其他行业的项目管理&#xff0c;建筑工程项目管理的周期一般更长&#xff0c;涉及部门更多&#xff0c;传统的管理方式无法照顾到方方面面。因此越来越多的工程团队希望能通过现代化数据管理工具来协助自己进行建筑工程项目管理。 正所谓有需求就有市场&#xff0c;目前市…