您现在的位置是:首页 >学无止境 >C++ 学习4网站首页学无止境

C++ 学习4

weixin_40273050 2023-05-12 20:15:03
简介(2) 如果是右值引用,由于被引用的对象是右值,它一般都是"临时对象",更不能长期保存。(1) 如果是左值引用可以视具体情况而定,左值引用。一般不行,除非能确定被引用的对象是长期存在的。

C++设计原则

高内聚低耦合

内聚就是一个模块内各个元素彼此结合的紧密程度,高内聚就是一个模块内各个元素彼此结合的紧密程度高。

所谓高内聚是指一个软件模块是由相关性很强的代码组成,只负责一项任务,也就是常说的单一责任原则。

耦合:一个软件结构内不同模块之间互连程度的度量(耦合性也叫块间联系。指软件系统结构中各模块间相互联系紧密程度的一种度量。模块之间联系越紧密,其耦合性就越强,模块的独立性则越差,模块间耦合的高低取决于模块间接口的复杂性,调用的方式以及传递的信息。)

常见方法:把要做的事情抽象成不同的虚类,然后分别继成实现。

比如:
Req:设计一个程序,从文件中读取数据,并处理该数据,然后打印输出到文件中。

a.没有设计的解决方案

设计一个类CDataProcessor来完成该任务:

  • 获取数据
  • 处理数据
  • 打印结果

该方案的缺点:

1、CDataProcessor有很多职责,不能在其他程序中重用该算法,

2、高耦合,处理过程和数据提供方和输出方高耦合。

b.有设计的解决方案

为了提高内聚程度,需要为每个职责定义不同的类,需要定义三个类,如下:

· CFileProvider: 用于从一个文件获取数据

· CDataProcessing: 处理数据,这个类可以用其他的类来完成数据处理。

· CResultReporting: 将结果输出到文件

这样每个类都有它自己的职责,这样做主要的好处有:

  1. 各类容易理解。

  2. 容易维护。

  3. 其他程序更好复用。

c. 低耦合

如果数据不是来自一个文件而是数据库,会发生什么样的情况?上面的设计对FileProvider存在高耦合。

为了解决这个问题,需要一个接口可以从任何地方获取数据,故需要定义一个接口类IDataProvider。
在这里插入图片描述

CDataProcessing 和 CReportResult两个类也可以按照上面的方法设计。

重构之后类之间的关系如下:
在这里插入图片描述
从上图可以看出,IDataProvider, IDataProcessing和IReportResult的创建都是main函数。更好的方法是把它的创建放在工厂类中,把这些类的实例化逻辑隔离开来。

控制器controller

上面各个类配合协调是在main函数实施的,更好的方法是在控制器中协调配合所有类,方便在其他应用程序中使用。

使用模板,该控制器可以这样实例化:

CController<CFileProvider,CDataProcessor,CConsoleReport>

SOLID原则

https://blog.csdn.net/qq_42672770/article/details/117650871

solid原则包括以下五个(详见上面的博客链接):

1、单一职责原则(SRP):表明一个类有且只有一个职责。一个类就像容器一样,它能添加任意数量的属性、方法等。

2、开放封闭原则(OCP):一个类应该对扩展开放,对修改关闭。这意味一旦创建了一个类并且应用程序的其他部分开始使用它,就不应该修改它。

3、里氏替换原则(LSP):派生的子类应该是可替换基类的,也就是说任何基类可以出现的地方,子类一定可以出现。值得注意的是,当通过继承实现多态行为时,如果派生类没有遵守LSP,可能会让系统引发异常。

4、接口隔离原则(ISP):表明类不应该被迫依赖他们不使用的方法,也就是说一个接口应该拥有尽可能少的行为,它是精简的,也是单一的。

5、依赖倒置原则(DIP):表明高层模块不应该依赖低层模块,相反,他们应该依赖抽象类或者接口。这意味着不应该在高层模块中使用具体的低层模块。

迪米特法则

通俗的讲就是:只和直接朋友交谈,不和陌生人说话。目的当然是降低偶合,提高独立性,修改仅影响直接相关的,降低耦合关系。
所谓的直接朋友指的是:当前对象自身、当前对象的成员对象、当前对象自己创建的对象、当前对象的方法中传入的参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象。
所谓的交谈指的是:可以访问这些朋友的成员。

如下代码就违背该原则, A 可以访问 b的方法,因为b是A的成员对象,但不应该访问 b的方法返回对象中的成员或者方法,带来的不好之处是 C中 value等修改,会影响到 A的修改。

class A {
public:
   int GetxxValue() {
        return b->GetC()->value;
   };
   B* b;
}
class B {
puulic:
   C* GetC();
   C* c;
}
class C {
publicint value;
}

重构为 B 中提供新的方法直接返回C的value,A中调用B的这个方法, C中的实现对于A隐藏。

“Tell, Don’t ask”原则

http://blog.csdn.net/zhongweijian/article/details/7825147
命令,不要去询问(Tell, Don’t Ask)”原则。这个原则讲的是,一个对象应该命令其它对象该做什么,而不是去查询其它对象的状态来决定做什么(查询其它对象的状态来决定做什么也被称作‘功能嫉妒(Feature Envy)’)。

这篇文章里有个很生动的例子:

if (person.getAddress().getCountry() == “Australia”) {

这违反了得墨忒耳定律,因为这个调用者跟Person过于亲密。它知道Person里有一个Address,而Address里还有一个country。它实际上应该写成这样:

if (person.livesIn(“Australia”)) {

组合 / 聚合复用原则

组合/ 聚合复用原则(Composition/Aggregation Reuse Principle )经常又叫合成复用原则(Composition Reuse Principle 或 CRP )。综是在一个新的对象里使用已有的对象,使之成为新对象的一部分,新的对象通过向这些对象的委派达到复用已有功能的目的。

该原则另一个简短的表述:尽量使用组合 / 聚合,不要使用继承。

只有当以下的条件全部被满足时,才应当使用继承关系,继承关系建议不要超过3层。
(1). 子类是超类的一个特殊种类,而不是超类的一个角色,也就是区分“Has-A”和“Is-A”.只有“Is-A”关系才符合继承关系,“Has-A”关系应当使用聚合来描述。

推荐使用场景:针对子类,如果父类的所有已实现的方法均适用于子类,并且父类需要抽象出抽象方法来供子类实现,这样的关系适用于继承。

(2) .永远不会出现需要将子类换成另外一个类的子类的情况。如果不能肯定将来是否会变成另外一个子类的话,就不要使用继承。

(3) .子类具有扩展超类的责任,而不是具有置换掉或注销掉超类的责任。如果一个子类需要大量的置换掉超类的行为,那么这个类就不应该是这个超类的子类。

扩展行为推荐子类实现父类的抽象方法(虚类),这样含义明确,父类不负责抽象方法的实现,父类会保持稳定;将不稳定部分下放到子类实现,当有多个子类时,每个子类只实现自己那部分方法,这样子类之间相互不影响,子类的实现就属于高内聚,子类只关注自己的实现,子类之间没有直接的依赖关系。

基础理论

三法则和五法则

三之法则(Rule of three): 若某个类需要用户定义的析构函数、拷贝构造函或拷贝赋值操作符,则它基本同时需要这三个函数。

五之法则(Rule of five): 如果定义了析构函数、拷贝构造函数或拷贝赋值操作符,会阻止移动构造函数和移动赋值操作符的隐式定义,所以任何想要移动语义的类必须声明全部五个特殊成员函数。

零之法则(Rule of zero): 如果类不需要专门处理资源的所有权,那么就不应该有自定义的析构函数、拷贝/移动构造函数或拷贝/移动赋值操作符。

class Foo {
public:
	Foo(const char* buffer, size_t size) { Init(buffer, size); }
	Foo(const Foo& other) { Init(other.buf, other.size); }
	Foo& operator=(const Foo& other)
	{
		Foo tmp(other);
		Swap(tmp);
		return *this;
	}
	Foo(Foo&& other) noexcept : buf(std::move(other.buf)), size(std::move(other.size))
	{
		other.buf = nullptr;
		other.size = 0;
	}
	Foo& operator=(Foo&& other) noexcept
	{
		Foo tmp(std::move(other));
		Swap(tmp);
		return *this;
	}
	~Foo() { delete[] buf; }
	void Swap(Foo& other) noexcept
	{
		using std::swap;
		swap(buf, other.buf);
		swap(size, other.size);
	}
	
private:
	void Init(const char* buffer, size_t size)
	{
		this->buf = new char[size];
		memcpy(this->buf, buffer, size);
		this->size = size;
	}
	char* buf;
	size_t size;
};

让对象支持移动

  • 对象应该有分开的拷贝构造和移动构造函数
    • 除非你只打算支持移动,不支持拷贝----如unique_ptr
  • 对象应该有 swap 成员函数,支持和另外一个对象快速交换成员;
  • 对象的命名空间下应当有自由的swap函数,调用成员函数swap来实现交换;
  • 实现通用的 operator=
    • 对于非常追求性能的场合,可能需要单独实现拷贝赋值和移动赋值运算符
  • 移动函数和swap函数不应抛异常,并应标记为 noexcept

为什么移动操作不应该抛异常?

https://blog.csdn.net/craftsman1970/article/details/104758487
抛出异常的移动操作会破坏大多数人的符合逻辑的推测。不会抛出异常的移动可以被标准库和C++语言更加高效地使用。

另外,对于自定义的移动构造和移动拷贝函数,必须要加上noexcept关键字。否则系统可能不会调用该移动函数。

C++ 中可以用左值引用或者右值引用保存数据吗?

一般不行,除非能确定被引用的对象是长期存在的。
(1) 如果是左值引用可以视具体情况而定,左值引用。
(2) 如果是右值引用,由于被引用的对象是右值,它一般都是"临时对象",更不能长期保存。

class A {
	......
}

class B {
public:
	A& a; // 不推荐,除非能确定别名a对应的左值生命周期大于本类的生命周期
	A&& aa; // 不推荐,想不出有什么场景可以适用,经测试会导致编译不过
}

引用折叠

对于一个给定类型X:

  • X& &、X& && 和 X&& & 都折叠成 X&
  • 类型 X&& &&折叠成 X&&

什么情况下,T&&有可能成为左值引用?

当T是模板参数时,T&&有可能成为左值引用,这是因为当T被推导为一个非引用类型时,T&&就会被推导为一个左值引用。例如:

template<typename T>
void foo(T&& t) {
    // ...
}

int main() {
    int x = 0;
    foo(x); // T被推导为int&,因此T&&被推导为int&
}

在这个例子中,当我们调用foo(x)时,T被推导为int&,因此T&&被推导为int& &&,即左值引用。这种情况下,我们可以使用std::forward(t)来保持参数的值类别。

上面这里例子也可以看出“T&& ”即可以接收左值类型的参数也可以接收右值类型的参数。

int& a; 和 int&& a中的a都是左值

class Obj;

Obj getObj(int n);

void foo(Obj& obj); // (1)

void foo(Obj&& obj); // (2)

int main()

{

  Obj&& r = getObj(42);

  foo(r);

}

标为 (1) 的 foo 将会被调用。因为右值引用的变量仍然是一个左值,会匹配重载 (1)。

函数能返回右值引用类型的参数吗?

虽然函数直接返回一个对象可以匹配相应的右值引用。但通常不应该使用 T&& 这样的返回值。返回为引用几乎永远是一种错误这是一种未定义行为!!!)。

生命期延长规则

左值引用和右值引用会延长被引用的变量的生命周期,最终同引用变量的生命周期一致。

std::swap

std::swap是C++ STL中的一个函数模板,用于交换两个变量的值。

template< class T >
void swap( T& a, T& b );

其中,a和b是要交换的两个变量。这个函数模板可以用于任何类型的变量,包括内置类型和自定义类型。

#include <iostream>
#include <algorithm>

int main()
{
    int a = 1;
    int b = 2;
    std::cout << "Before swap: a = " << a << ", b = " << b << std::endl;
    std::swap(a, b);
    std::cout << "After swap: a = " << a << ", b = " << b << std::endl;
    return 0;
}

使用std::swap的优点

  • 通用性:std::swap是一个函数模板,可以用于任何类型的变量,包括内置类型和自定义类型。
  • 高效性:**对于内置类型,std::swap可以直接交换它们的值,而不需要进行复制操作。**对于自定义类型,可以通过特化std::swap来提高效率。
  • 可读性:使用std::swap可以使代码更加简洁易懂。

C++实战

C++17 编译

测试自己的小程序时,如果用到了C++17的语法,编译时要加上"-std=c++17", 如:

g++ main.cpp -std=c++17

boost正则匹配

参考:
基础:https://blog.csdn.net/u012198575/article/details/83624251
详尽:https://blog.csdn.net/ponew/article/details/80348352

regex_match()函数用来判断是否匹配,regex_match要求正则express与串全部匹配时才返回true,否则返回false;
regex_search()函数来搜索字符串中的匹配项,并使用smatch对象来存储匹配结果。regex_search不要求正则express与串完全匹配,但串中内容可以多不能少,即字符串中必须要有定义的正则express才会返回true

匹配时默认是贪婪模式,可以添加’?‘选择尽可能短的匹配,如’.?';
(?:pattern):匹配pattern但不获取匹配结果;
将上面两个结合起来:(?:.
?)这个表达式表示不储存(捕获)结果,并且不贪婪。
表示可以匹配0次或者多次,如果希望至少匹配1次,则不要用,例如用(.?)替换(.*?);

注意:(.)是贪婪匹配,(.?)是非贪婪匹配,比如对于字符串"aaaaa key bbbbbb key":
“(.)key" 捕获的是 "aaaaa key bbbbbb ",能要的我全要,只给你留最后一个
"(.
?)key” 捕获的是"aaaaa ",我不贪婪,只要捕获第一个能满足正则表达式的字符串我就返回

#include <iostream>
#include <string>
#include <boost/regex.hpp>

using namespace std;
using namespace boost;

int main()
{
    string str = "2023-03-28 14:37:52.891 Sessiona,adfadfadf";
    regex reg("(\d{4}-\d{2}-\d{2})\s(\d{2}:\d{2}:\d{2}\.\d{3})");
    smatch m;

	// 注意:只有正则表达式全部满足才会返回true,即定义的格式字符串中必须要有
    if (regex_search(str, m, reg)) 
    {
    	if {m[1].matched) {
    		cout << "日期内容:" << m[1] << endl;
    	}
        if {m[2].matched) {
        	cout << "时间内容:" << m[2] << endl;
        }
    }
	
	string str2 = "2023-03-28 14:37:52.891 Session "My-Session-Name": xxxxxxxxxx: Client host name: "", Client IP address: "10.2.2.100", Client port number: xxxx";
	
    regex reg2("Session\s"(.*?)":.*?Client IP address:\s"(.*?)"");
    smatch match2;
    if (regex_search(str2, match2, reg2))
    {
        cout << "session-name:" << match2[1] << endl;
        cout << "targetIP:" << match2[2] << endl;
    }
    return 0;
}

分析reg:
(1) ()表示捕获,要输出到外面的内容;
(2) ‘d’即为d,表示匹配数字;
(3) ‘.’即为‘.’,表示匹配’.‘;(如果没有‘’,只有单独的‘.’则表示匹配任意字符)
(4) {4}表示长度必须为4;
(5) ‘s’即为s,表示匹配空格;
(6) 正则表达式Sessions"(.*?)“:.Client IP address:s"(.?)”,它匹配Session后面的任何内容,然后用‘()’捕获,再继续匹配直到找到’Client IP address’,然后继续用‘()’捕获,获取IP。

另外一个例子,如何匹配"sec_20230413_0737.log"中的日期和时间?

std::string fileName1 = "sec_20230413_0737.log";
boost::regex reg(".*?(\d{1,8})_(\d{1,6})\.log");
boost::smatch m1;
regex_search(fileName1, m1, reg);

注意最前面的.*?不能改成.*,否则可能会因为贪婪只能匹配到最后一个数字(期望是能匹配1-8个数字)。

std::map—key和value分别为左值和右值时的插入性能测试

背景:为了解STL中右值版本比左值版本的插入性能对比,因为理论上左值版本插入时要多一次拷贝构造和对象析构,故性能会下降。(gcc版本为10.4.0,使用较新的C++17规则)
当map类型为std::map<std::vector<std::string>, std::string> map时,分别用左值和右值测试10000、100000、100000次添加数据,测试代码如下:

std::cout << "Test for "std::map<std::vector<std::string>, std::string> map" begin:" << std::endl;
std::vector<int> IterTimes = {10000, 100000, 1000000};
for (size_t t = 0; t < IterTimes.size(); t++) {
    int times = IterTimes[t];
    std::chrono::nanoseconds leftDura, rightDura;
    {
        std::map<std::vector<std::string>, std::string> mp;
        auto start = std::chrono::steady_clock::now();
        for (int i = 0; i < times; i++) {
            auto key = std::vector<std::string>{{std::to_string(i), "testNamekey"}};
            auto value = std::to_string(i) + "testValue";
            mp[key] = value;
        }
        leftDura = std::chrono::nanoseconds(std::chrono::steady_clock::now() - start);
        std::cout << "Test left copy for std::map: Duration Time = " << leftDura.count() << " ns" << std::endl;
    }

    {
        std::map<std::vector<std::string>, std::string> mp;
        auto start = std::chrono::steady_clock::now();
        for (int i = 0; i < times; i++) {
            mp[std::vector<std::string>{{std::to_string(i), "testNamekey"}}] = std::to_string(i) + "testValue";
        }
        rightDura = std::chrono::nanoseconds(std::chrono::steady_clock::now() - start);
        std::cout << "Test right copy for std::map : Duration Time = " << rightDura.count() << " ns" << std::endl;
    }

    std::cout << "Iter Times = " << times << ", R-copy is (" << (leftDura.count() - rightDura.count()) * 1.0 / rightDura.count() << "%) faster than L-copy.
" << std::endl;
}

执行结果:
在这里插入图片描述
可以看出右值版本要比左值版本快7%-40%,至于为什么迭代次数到达1000000次时,反而变少了,猜测是因为std::map的红黑树构造太耗时了。

当map类型换成std::map<std::string, std::string> map时,类似的代码执行结果如下:
在这里插入图片描述
神奇地发现右值版本居然没有比左值版本快,推测是因为C++11后,对std::string类型的字符串的拷贝做了优化,性能开销几乎可以忽略不计。进而推测第一个例子中左值拷贝的开销大头是来自于key的类型std::vector<std::string>的拷贝析构。

总结std::map的使用:
(1) 当key或者value的拷贝构造较复杂时,使用右值插入会比左值插入有较大的性能提升!!!
(2) 反之,当key或者value的拷贝构造开销很低时,两者性能相当。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。