北京大学程序设计MOOC作业详解-02-类和对象基础(下)

文章探讨了在C++中处理带有特定分隔符的输入数据格式问题,包括cin、scanf、stringstream和strtok等方法的优缺点。针对姓名可能由一个或两个单词组成的情况,提出了使用stringstream和strtok函数的解决方案,并提醒了使用strtok的线程安全问题。文章还介绍了如何解析字符串到结构体和输出JSON格式的信息。最后,讨论了C++类的构造函数和赋值操作符在特定问题中的应用。
摘要由CSDN通过智能技术生成

北京大学程序设计MOOC作业详解-02-类和对象基础

第六题继续:

题目内容详情请见:类和对象基础(上)

为了完成本题目,我们在编程实践时遇到了一个困难:输入数据格式问题。这是因为单纯用cin或者scanf/sscanf会遇到麻烦,分析如下:

首先,明确输入的内容及格式,样例如下:

Tom Hanks,18,7817,80,80,90,70

很明显,姓名的姓氏和名字用空格隔开,年龄、学号及四年成绩用逗号隔开,这是麻烦的根源,下面分析几种不同的输入方式。

第一种,cin输入
void input() {
		string name1, name2;
		cin >> name1 >> name2 >> age >> stuId >> sc1 >> sc2 >> sc3 >> sc4;
		name = name1 + " " + name2;
	}

cin是一种遇到空格会停止读取的输入方式,这意味着,实际读入了两个字符串,即:

Tom
Hanks,18,7817,80,80,90,70

这显然不对,会给“年龄,学号,成绩”等成员变量赋予一个随机的值。

第二种,scanf格式输入

这里直接插入scanf的输入代码,根据格式要求,理应输入:

scanf("%s %s %d,%s,%d,%d,%d,%d", 
	name1, name2, &age, stuId, &sc1, &sc2, &sc3, &sc4);

但是,scanf和cin一样,要么完全按照空格分割输入内容,要么没有空格完全按照格式输入内容。

因为:如果既有空格又有逗号,程序无法区别,第二个串Hanks,18,7817,80,80,90,70到底是name2还是其他内容的格式输入信息。这种歧义会让程序认为它是name2。

第三种,stringstream流完成字符串分割
void input() {
		string inp, *temp = new string[7]();
		cin >> name >> inp;
		stringstream ss(inp);
		int idx = 0;
		while (getline(ss, temp[idx], ',')) {
			++idx;
		}
		if (idx == 7) {
			name += (" " + temp[0]);
			age = stoi(temp[1]);
			stuId = temp[2];
			sc1 = stoi(temp[3]);
			sc2 = stoi(temp[4]);
			sc3 = stoi(temp[5]);
			sc4 = stoi(temp[6]);
		}
		delete[] temp;
	}

总结stringstream在分割字符串中的用法,总体来说,分为两步:

stringstream ss(str_line);// 所有输入字符串,初始化流
vector<string> tokens;
string cur_str;
while (getline(ss, cur_str, ',') {
	tokens.emplace_back(move(cur_str));// 减少拷贝
	/*
		上述代码使用移动语义减少拷贝,功能上等价于:
		tokens.push_back(cur_str);
	*/
}

但是,在这个题中有一个重大BUG,姓名并非一定有空格隔开姓氏与名字,也可以只有一个名字,即:

Tom Hanks
Vogon

均是可以输入的姓名,一旦是后者(Vogon),那就不存在空格,在这行代码会出现BUG:cin >> name >> inp;,因为这里默认有姓氏和名字了。应该这样修改:

void input() {
		string inpInfo, cur_info;
		vector<string> temp;
		getline(cin, inpInfo);
		stringstream ss(inpInfo);
		while (getline(ss, cur_info, ',')) {
			temp.emplace_back(move(cur_info));
		}
		if (temp.size() == 7) {
			name = temp[0];
			age = stoi(temp[1]);
			stuId = temp[2];
			sc1 = stoi(temp[3]);
			sc2 = stoi(temp[4]);
			sc3 = stoi(temp[5]);
			sc4 = stoi(temp[6]);
		}
	}

值得注意的是,会有一个警告报出:“使用已移动的from对象”,这是从代码块:

while (getline(ss, cur_info, ',')) {
	temp.emplace_back(move(cur_info));
}

报出来的,这是因为移动语义是将cur_info的所有权移交给temp.back()了,即向量temp的尾部指针/对象。这个警告需要管吗?在标准库下不用管。可以参考《C++ Primier》的一句话:
在这里插入图片描述
也就是说,被移动的对象的状态,由上下文决定。也就是说,只要cur_info在合法作用域内,那移动后的cur_info仍然是合法的状态,即可以赋值。

第四种,strtok函数及应用

第三种,用stringstream字符串流分割字符串,已经可以满足需求了。即使题目并不支持vector库,但是也可以改用string数组完成,这里不再赘述。

本节,给大家介绍strtok函数的应用,以及其线程安全与源码分析。

strtok函数在本题的解法
char *strtok(char *str, const char *delim)

这是strtok的函数声明,其中str是待分割的字符串,delim是分隔符,以该字符串为分割依据进行分割,然后返回当前分割的结果。

本题运用strtok的AC代码如下:

private:
	string name;
	string stuId;
	int age, sc[4];
	double score;
public:
	void input() {
		char infoBuf[128] = { '\0' };
		cin.getline(infoBuf, 128);
		int idx = 0;
		char* cur_str = strtok(infoBuf, ",");
		while (cur_str) {
			switch (idx)
			{
			case 0:
				name = cur_str;
				break;
			case 1:
				age = atoi(cur_str);
				break;
			case 2:
				stuId = cur_str;
				break;
			case 3:
			case 4:
			case 5:
			case 6:
				sc[idx - 3] = atoi(cur_str);
				break;
			default:
				break;
			}
			cur_str = strtok(nullptr, ",");
			++idx;
		}
	}

	void calculate() {
		score = (sc[0] + sc[1] + sc[2] + sc[3]) / 4.0;
	}

	void output() {
		cout << name << "," << age << "," << stuId << "," << score << endl;
	}
strtok函数的应用

从上面的例子可以总结,使用strtok函数分为两步

// 第一步:分割字符串,并将剩余字符串转存到一个static变量中
char* cur_str = strtok(src_str, delim);

while (cur_str) {
	cout << cur_str << endl;
	// 第二步:无需转存,所以传入nullptr,只需在static变量中分割子串
	cur_str = strtok(nullptr, delim);
}

需要注意的是,由于是用静态变量存储剩余的字符串,在多线程下就不安全了。这是因为:一个线程分割了字符串,另一个线程就得不到原始的字符串了,而是分割后剩下的子串。参考网上的资料可知:

使用该函数进行字符串分割时,会破坏被分解字符串的完整,调用前和调用后的s已经不一样了。第一次分割之后,原字符串str是分割完成之后的第一个字符串,剩余的字符串存储在一个静态变量中

这也是为什么,后续不用再传递str给函数的原因,因为str并不包含剩余子串的任何信息了。

做一道简单的应用题:解析一个字符串,并存储到结构体中
【输入】输入一个任意长的字符串,按照如下格式:

Aob male 18,Bob male 19,Cob female 20

【输出】输出json的格式化信息,例如:

{
	{
		name: Aob,
		sex: male,
		age: 18
	},
	{
		name: Bob,
		sex: male,
		age: 19
	},
	{
		name: Cob,
		sex: female,
		age: 20
	}
}

这个题有一个困难:第一次解析,得到第一个人的信息后,剩余子串存储在全局静态的变量当中。然后第二次解析,解析学生个人的信息时,又改变了静态变量,导致丢失了剩余子串。

为了避免这个静态变量,我们需要使用线程安全的strtok,即Linux下用strtok_r,Windows下用strtok_s。

一个BUG:使用strtok_r/strtok_s时,输入的被分割字符串str不能是字符串常量,否则会段错误!

用法示例如下:

char* ctx = nullptr;
char* cur_str = strtok_s(buf, ",", &ctx);
while (cur_str) {
	/*
		再声明一个上下文,用来存储子串的分割剩余串
	*/
	cur_str = strtok_s(nullptr, ",", &ctx);
}

本练习题的代码如下:

#define _CRT_SECURE_NO_WARNINGS
#include <cstring>
#include <vector>
#include <string>
#include <iostream>
using namespace std;

struct Student {
	string m_name;
	string m_sex;
	uint32_t m_age;

	Student(char* buf) {
		readStudent(buf);
	}

	void jsonPrinter() const {
		cout << "\t{\n";
		cout << "\t\tname: " << m_name << endl;
		cout << "\t\tsex : " << m_sex << endl;
		cout << "\t\tage : " << m_age << endl;
		cout << "\t}\n";
	}

	void readStudent(char* buf) {
		char* ctx_info = nullptr;
		char* cur_info = strtok_s(buf, " ", &ctx_info);
		uint32_t cur_idx = 0;
		while (cur_info) {
			if (0 == cur_idx) { m_name = cur_info; }
			else if (1 == cur_idx) { m_sex = cur_info; }
			else if (2 == cur_idx) { m_age = stoul(cur_info); }
			else { break; }
			cur_info = strtok_s(nullptr, " ", &ctx_info);
			++cur_idx;
		}
	}
};

void studentJsonPrinter(const vector<Student>& students) {
	cout << "{\n";
	for (auto& stu : students) {
		stu.jsonPrinter();
	}
	cout << "}\n";
}

int main() {
	vector<Student> students;
	char stu_buf[1024] = { '\0' };
	cin.getline(stu_buf, 1024);
	char* ctx_stu = nullptr;
	char* cur_stu = strtok_s(stu_buf, ",", &ctx_stu);
	while (cur_stu) {
		students.emplace_back(cur_stu);
		cur_stu = strtok_s(nullptr, ",", &ctx_stu);
	}
	studentJsonPrinter(students);
	return 0;
}

运行结果如下:
在这里插入图片描述

好了,暂时就分析到这了,进一步分析strtok的源码和线程安全,请移步下一篇博客

最后一个需要注意的小点

这个题,在输出四年平均成绩(浮点数)的时候是遵循:有几位小数,就输出几位小数的。并非保留整数部分,也并非其他情况。所以,不建议用printf格式输出,而是直接用cout,程序会自行打印到具体的小数位

第七题:

在这里插入图片描述

第七题分析:

这个题要求输出9 22 5,但是题目实例化对象中,输入的数据是5,20,5,如何才能得到9 22 5呢?首先,我们要分析Sample类需要补充哪些函数,以及main函数会调用哪些函数。

main函数会调用哪些函数?

Sample a(5); // 调用有参构造
Sample b = a; // 调用拷贝构造
PrintAndDouble(b); // 调用拷贝构造
Sample d;// 调用无参构造
d = a; // 调用赋值操作符函数

需要补充Sample类的哪些函数?

构造:无参构造和有参构造可以一起写,把有参改成默认值为0即可。
赋值:类会有一个默认的,浅拷贝的赋值操作符函数,这里看实际情况要不要重写。

如何得到9 22 5?

我们发现,9是通过调用PrintAndDouble(b);得到的,这里有两次拷贝构造。最开始的拷贝构造Sample b = a; 是将a的值赋给b,这里b的值是5。不难想象,一次拷贝构造,会在原本值的基础上加2,连续两次拷贝则加4,因此得到9。

如何,按照上面的分析,打印c时会有拷贝构造,因此又加2,得到22。而5并没有发生变化,因此无需重写赋值运算操作符。

本题AC的代码如下:

Sample(int v_ = 0) : v(v_) {  }
Sample(const Sample& s) : v(s.v + 2) {  }

这里不禁有同学会问,为什么不要重写赋值操作运算符呀?难道不会有浅拷贝危害吗?这里要强调的是,浅拷贝危害,只会出现在成员变量是指针变量的情况!因为:浅拷贝危害是由指针直接赋值而带来的潜在危害

第八题:

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

第八题分析:

首先,一定会输出一个123,然后不能改main函数,那么123从何而来?可能是从构造函数得来。

然后,输入的m和n,都会变成a的成员变量val,这显然是改变了value的值,并且是通过GetObj这样的函数返回值改动的,这说明:GetObj的返回值是引用类型

然后,GetObj既能接收一个整数m,又能接收一个对象A(n),说明返回值是A&类型。一个整数m会调用赋值构造函数(即构造函数),但是这个构造函数显然没有输出123,说明这里的有参构造不能和无参构造写成一个函数。

因此,本题需要有补充三个函数:
1)一个无参构造函数,将val赋予默认值(可不做),并输出123;
2)一个有参构造函数,用来将整数m变成对象A(m);
3)一个可改变this指向的对象的函数GetObj,返回*this,类型是A&。

AC的代码如下(为了看起来美观,将整个类A的设计放进来了):

class A {
public:
	int val;

	A(int
		v = 123) : val(v) { }

	A& GetObj() { return *this; }

	A& operator=(const A& obj) {
		if (this == &obj) { return *this; }
		val = obj.val;
		return *this;
	}
};

第九题:

在这里插入图片描述

第九题分析:

这个题只需要添加构造函数赋值运算操作符函数即可,在构造函数中初始化成员变量 r 和 i 。但是这个题很多同学写的太简单了,直接用写:

Complex(const char* str = nullptr) {
    if (!str) { 
    	r = i = 0.0;
    	return; 
    }
	r = (double)(str[0]-'0');
	i = (double)(str[2]-'0');
}

这里可能同学们有疑问,main函数里面的函数调用明明是:
1)调用无参构造;
2)调用有参构造,初始化匿名对象
3)调用赋值操作运算符函数

这里不实现1)无参构造还可以理解,因为有默认值nullptr了,注意默认值就要有默认值的处理逻辑!那为什么不实现3)赋值操作运算符的重载呢?

之前提过,这里再次强调:C++的每一个类,都会有一个默认的,浅拷贝的赋值操作运算符函数。然而,这个类Complex,没有成员指针变量,因此可以用浅拷贝(即默认)的赋值操作运算符函数

第九题的升级版

现在要求,对于任意的输入,甚至可能包含空格,解析出复数,例如:
【输入】

1.234+2.5678i
      8765i
  45.1024
245412+365102.855i

【输出】

1.234+2.5678i
0+8765i
45.1024+0i
245412+365102.855i

这本质上是一个字符串分割和字符串转浮点数的问题,如果要用strtok_r或者strtok_s(根据系统而定)可能会出现段错误的问题,这里需要注意:使用线程安全的字符串分割strtok函数,不能输入字符串常量作为被分割的串

代码实现如下:

Complex(const char* str = nullptr) : r(0), i(0) {
        if (!str) { return; }
        char* buf = new char[strlen(str) + 1]();
        strcpy(buf, str);
        char* ctx1 = nullptr, * ctx2 = nullptr;
        char* cur_r = strtok_s(buf, "+", &ctx1);
        r = atof(cur_r);
        char* cur_i = strtok_s(ctx1, "i", &ctx2);
        i = atof(cur_i);
        delete[] buf;
        buf = nullptr;
    }

代码提交时,需要注意:OJ测评机是Linux系统,应使用strtok_r而并非strtok_s,只有在Windows本地编写代码时,才用strtok_s。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值