拿你有的,换你要的;这个世界一直如此,很残酷,却很公平!

C++标准库_语言新特性

如果使用QQ、微信、csdn等app打开本网页,UI可能加载出现问题,建议使用手机浏览器,体验更加,哈哈,不辜负美好的加载界面和动画

鸡汤一天只建议一条,过度服用有害身体健康。特此声明。

鸡汤链接:伤春悲秋,无病呻吟几句 - 波波神2013 - 博客园 (cnblogs.com)

让爱和恨支持着你前进。

失败只有一种可能,就是半途而废

先生独坐,春风翻书。小暑过后,春风由在。此后经年,随风去。

格局打开,你会发现很多事情根本不会影响你的情绪。学习学习。

又不想学了,太TM累了,来点鸡汤喝喝。

一个人知道自己为什么而活,就可以忍受任何一种生活。

学习太累了,听个苏州评弹。 上链接:苏州评弹 | 合集_全集免费在线阅读收听下载 - 喜马拉雅 (ximalaya.com)

你的假装努力,欺骗的只有你自己,永远不要用战术上的勤奋,来掩饰战略上的懒惰。

只有坚持别人无法坚持的,才能拥有别人无法拥有的。

cpp标准库学习

时光转瞬即逝,最近一段时间由于个人原因,非常颓废和难受,反省自己度过了大半年空虚时光,没有学习,我感觉自己忘记了自己曾经的目标,决定给自己打打鸡血了,恍惚之间,还是学习吧。

第一章和第二章略过。

第一章和第二章只是概述,可以迅速略看。


第三章:语言的新特性

3.1 0,NULL和nullptr的区别

一.在C语言中 NULL其实就是一个宏(表示把 0 强制类型转换成void *)

define NULL (void *)0

char *p = NULL; 

二.在C++中

define NULL 0

char *p = NULL;

char *q = nullptr;

那么问题来了为什么 C++中 NULL 和 C中的 NULL 表示的宏的意义不同呢? 难道C++中的NULL 不可以表示 (void *)0吗?

是这样的: C++是一门 严格校验类型的语言,它不允许(void *)隐式转换成其他类型,如果你真要这么干

define NULL (void *)0 在C++中,当编译 char *p = NULL; 时 编译就会出错。所以NULL 只好被定义成 0 但是这种方式会产生二义性:

void func(int num){
       cout << " int " << endl;

}

void func(char *ptr){
       cout << " pointer " << endl;

}

当我们调用 func(NULL)的时候 编译器到底会去调用哪个函数呢? NULL是 0 的一个替换 , 也表示一个空指针 这里两个函数好像都满足条件 所以 这就是我们所说的二义性 。

因此为了解决上面的问题 C++11 引入了一个 新的空指针 nullptr 专门用来区分 0 和 NULL的

func(0), func(nullptr)这样才会合理。

nullptr是 nullptr_t 类型 不是整数类型 可以隐式转换成任意类型的 指针,所以在这里 我推荐大家用nullptr


3.2 以当auto完成类型自动推导

​ 以auto声明的变量,其类型会根据其初值被自动推导出来,因此一定需要一个初始化操作。

static auto i;

3.3 一致性初始化(Uniform Initialization)与初始列(Initializer List)

一致性初始化

int values[] {};

无所不能的大括号。

正常初始化都是 定义 类型 名称 {};这种初始化被称为Uniform Initialization。

int *p; 与int *p{}; 前者未定义初值,后者被初始化为nullptr.

为了避免“窄化”(精度降低或数值变动),依赖初值设定(initializer)的实际值(actual value),而非只是依赖类型。

如果是用户自定义的类型:class template std::initializer_list vals,从而实现初始化。初值列是提供了成员函数begin()和end(),便于你使用range-based for。


3.4 for循环 range-based for

for( int i:{2,5,4...,1,7,9} );

for( decl: coll)  collection)   //1
for( auto _pos=coll.begin(); _pos != coll.end(); ++_pos )//2
for( auto _pos=coll.begin(), _end=coll.end(); _pos != _end(); ++_pos )//3  //当coll提供成员函数begin,start的情况下,与1等价。
for( auto _pos=begin(coll), _end=end(coll); _pos != _end(); ++_pos ) //4  

for( auto &elem :vec) //std::vector<double> vec;

声明elem为一个引用很重要,这里可以google一下“指针与引用的区别”,

可以对初值列(initializer list)使用range-based for循环。


3.5 Move semantic(搬迁语义) & Rvalue Reference(右值引用)

Cpp设计目标内:避免copy和temporary操作的产生。

insert操作:对于insert操作,set& coll 内部无需为它建立一份copy且以“某种方式move其内容进入新建元素中”。

声明<utility>中的std:move(x) 
//moves contents of x into coll, x可以被move而非copied。它只是将实参转换为一个所谓的rvalue reference.

右值引用的意义通常解释为两大作用:移动语义和完美转发。

移动语义

======

移动语义,简单来说解决的是各种情形下对象的资源所有权转移的问题。而在C++11之前,移动语义的缺失是C++饱受诟病的问题之一。

举个栗子。

问题一:如何将大象放入冰箱?

答案是众所周知的。首先你需要有一台特殊的冰箱,这台冰箱是为了装下大象而制造的。你打开冰箱门,将大象放入冰箱,然后关上冰箱门。

问题二:如何将大象从一台冰箱转移到另一台冰箱?

普通解答:打开冰箱门,取出大象,关上冰箱门,打开另一台冰箱门,放进大象,关上冰箱门。

2B解答:在第二个冰箱中启动量子复制系统,克隆一只完全相同的大象,然后启动高能激光将第一个冰箱内的大象气化消失。

等等,这个2B解答听起来很耳熟,这不就是C++中要移动一个对象时所做的事情吗?

“移动”,这是一个三岁小孩都明白的概念。将大象(资源)从一台冰箱(对象)移动到另一台冰箱,这个行为是如此自然,没有任何人会采用先复制大象,再销毁大象这样匪夷所思的方法。C++通过拷贝构造函数和拷贝赋值操作符为类设计了拷贝/复制的概念,但为了实现对资源的移动操作,调用者必须使用先复制、再析构的方式。否则,就需要自己实现移动资源的接口。

为了实现移动语义,首先需要解决的问题是,如何标识对象的资源是可以被移动的呢?这种机制必须以一种最低开销的方式实现,并且对所有的类都有效。C++的设计者们注意到,大多数情况下,右值所包含的对象都是可以安全的被移动的。

右值(相对应的还有左值)是从C语言设计时就有的概念,但因为其如此基础,也是一个最常被忽略的概念。不严格的来说,左值对应变量的存储位置,而右值对应变量的值本身。C++中右值可以被赋值给左值或者绑定到引用。类的右值是一个临时对象,如果没有被绑定到引用,在表达式结束时就会被废弃。于是我们可以在右值被废弃之前,移走它的资源进行废物利用,从而避免无意义的复制。被移走资源的右值在废弃时已经成为空壳,析构的开销也会降低。

右值中的数据可以被安全移走这一特性使得右值被用来表达移动语义。以同类型的右值构造对象时,需要以引用形式传入参数。右值引用顾名思义专门用来引用右值,左值引用和右值引用可以被分别重载,这样确保左值和右值分别调用到拷贝和移动的两种语义实现。对于左值,如果我们明确放弃对其资源的所有权,则可以通过std::move()来将其转为右值引用。std::move()实际上是static_cast<T&&>()的简单封装。

右值引用至少可以解决以下场景中的移动语义缺失问题:

  • 按值传入参数

按值传参是最符合人类思维的方式。基本的思路是,如果传入参数是为了将资源交给函数接受者,就应该按值传参。同时,按值传参可以兼容任何的cv-qualified左值、右值,是兼容性最好的方式。

class People {
public:
  People(string name) // 按值传入字符串,可接收左值、右值。接收左值时为复制,接收右值时为移动
  : name_(move(name)) // 显式移动构造,将传入的字符串移入成员变量
  {
  }
  string name_;
};

People a("Alice"); // 移动构造name

string bn = "Bob";
People b(bn); // 拷贝构造name

构造a时,调用了一次字符串的构造函数和一次字符串的移动构造函数。如果使用const string& name接收参数,那么会有一次构造函数和一次拷贝构造,以及一次non-trivial的析构。尽管看起来很蛋疼,尽管编译器还有优化,但从语义来说按值传入参数是最优的方式。

如果你要在构造函数中接收std::shared_ptr并且存入类的成员(这是非常常见的),那么按值传入更是不二选择。拷贝std::shared_ptr需要线程同步,相比之下移动std::shared_ptr是非常轻松愉快的。

  • 按值返回

和接收输入参数一样,返回值按值返回也是最符合人类思维的方式。曾经有无数函数为了返回容器而不得不写成这样

void str_split(const string& s, vector<string>* vec); // 一个按值语义定义的字符串拆分函数。这里不考虑分隔符,假定分隔符是固定的。

这样要求vec在外部被事先构造,此时尚无从得知vec的大小。即使函数内部有办法预测vec的大小,因为函数并不负责构造vec,很可能仍需要resize。

对这样的函数嵌套调用更是痛苦的事情,谁用谁知道啊。

有了移动语义,就可以写成这样

vector<string> str_split(const string& s) {
  vector<string> v;
  // ...
  return v; // v是左值,但优先移动,不支持移动时仍可复制。
}

如果函数按值返回,return语句又直接返回了一个栈上的左值对象(输入参数除外)时,标准要求优先调用移动构造函数,如果不符再调用拷贝构造函数。尽管v是左值,仍然会优先采用移动语义,返回vector从此变得云淡风轻。此外,无论移动或是拷贝,可能的情况下仍然适用编译器优化,但语义不受影响。

对于std::unique_ptr来说,这简直就是福音。

unique_ptr<SomeObj> create_obj(/*...*/) {
  unique_ptr<SomeObj> ptr(new SomeObj(/*...*/));
  ptr->foo(); // 一些可能的初始化
  return ptr;
}

当然还有更简单的形式

unique_ptr<SomeObj> create_obj(/*...*/) {
  return unique_ptr<SomeObj>(new SomeObj(/*...*/));
}

在工厂类中,这样的语义是非常常见的。返回unique_ptr能够明确对所构造对象的所有权转移,特别的,这样的工厂类返回值可以被忽略而不会造成内存泄露。上面两种形式分别返回栈上的左值和右值,但都适用移动语义(unique_ptr不支持拷贝)。

  • 接收右值表达式

没有移动语义时,以表达式的值(例为函数调用)初始化对象或者给对象赋值是这样的:

vector<string> str_split(const string& s);

vector<string> v = str_split("1,2,3"); // 返回的vector用以拷贝构造对象v。为v申请堆内存,复制数据,然后析构临时对象(释放堆内存)。
vector<string> v2;
v2 = str_split("1,2,3"); // 返回的vector被复制给对象v(拷贝赋值操作符)。需要先清理v2中原有数据,将临时对象中的数据复制给v2,然后析构临时对象。

注:v的拷贝构造调用有可能被优化掉,尽管如此在语义上仍然是有一次拷贝操作。

同样的代码,在支持移动语义的世界里就变得更美好了。

vector<string> str_split(const string& s);

vector<string> v = str_split("1,2,3"); // 返回的vector用以移动构造对象v。v直接取走临时对象的堆上内存,无需新申请。之后临时对象成为空壳,不再拥有任何资源,析构时也无需释放堆内存。
vector<string> v2;
v2 = str_split("1,2,3"); // 返回的vector被移动给对象v(移动赋值操作符)。先释放v2原有数据,然后直接从返回值中取走数据,然后返回值被析构。

注:v的移动构造调用有可能被优化掉,尽管如此在语义上仍然是有一次移动操作。

不用多说也知道上面的形式是多么常用和自然。而且这里完全没有任何对右值引用的显式使用,性能提升却默默的实现了。

  • 对象存入容器

这个问题和前面的构造函数传参是类似的。不同的是这里是按两种引用分别传参。参见std::vector的push_back函数。

void push_back( const T& value ); // (1)
void push_back( T&& value ); // (2)

不用多说自然是左值调用1右值调用2。如果你要往容器内放入超大对象,那么版本2自然是不2选择。

vector<vector<string>> vv;

vector<string> v = {"123", "456"};
v.push_back("789"); // 临时构造的string类型右值被移动进容器v
vv.push_back(move(v)); // 显式将v移动进vv

困扰多年的难言之隐是不是一洗了之了?

  • std::vector的增长

又一个隐蔽的优化。当vector的存储容量需要增长时,通常会重新申请一块内存,并把原来的内容一个个复制过去并删除。对,复制并删除,改用移动就够了。

对于像vector这样的容器,如果频繁插入造成存储容量不可避免的增长时,移动语义可以带来悄无声息而且美好的优化。

  • std::unique_ptr放入容器

曾经,由于vector增长时会复制对象,像std::unique_ptr这样不可复制的对象是无法放入容器的。但实际上vector并不复制对象,而只是“移动”对象。所以随着移动语义的引入,std::unique_ptr放入std::vector成为理所当然的事情。

容器中存储std::unique_ptr有太多好处。想必每个人都写过这样的代码:

MyObj::MyObj() {
  for (...) {
    vec.push_back(new T());
  }
  // ...
}

MyObj::~MyObj() {
  for (vector<T*>::iterator iter = vec.begin(); iter != vec.end(); ++iter) {
    if (*iter) delete *iter;
  }
  // ...
}

繁琐暂且不说,异常安全也是大问题。使用vector<unique_ptr>,完全无需显式析构,unqiue_ptr自会打理一切。完全不用写析构函数的感觉,你造吗?

unique_ptr是非常轻量的封装,存储空间等价于裸指针,但安全性强了一个世纪。实际中需要共享所有权的对象(指针)是比较少的,但需要转移所有权是非常常见的情况。auto_ptr的失败就在于其转移所有权的繁琐操作。unique_ptr配合移动语义即可轻松解决所有权传递的问题。

注:如果真的需要共享所有权,那么基于引用计数的shared_ptr是一个好的选择。shared_ptr同样可以移动。由于不需要线程同步,移动shared_ptr比复制更轻量。

  • std::thread的传递

thread也是一种典型的不可复制的资源,但可以通过移动来传递所有权。同样std::future std::promise std::packaged_task等等这一票多线程类都是不可复制的,也都可以用移动的方式传递。

完美转发

======

除了移动语义,右值引用还解决了C++03中引用语法无法转发右值的问题,实现了完美转发,才使得std::function能有一个优雅的实现。这部分不再展开了。

总结

======

移动语义绝不是语法糖,而是带来了C++的深刻革新。移动语义不仅仅是针对库作者的,任何一个程序员都有必要去了解它。尽管你可能不会去主动为自己的类实现移动语义,但却时时刻刻都在享受移动语义带来的受益。因此这绝不意味着这是一个可有可无的东西。


3.6 String Literal

一、什么是Raw String?

C++ Raw String是C++ 11提供的原始(未加工)的字符串的用法。

原始字符串简单来说,“原生的、不加处理的”,字符表示的就是自己(所见即所得),引号、斜杠无需 “\” 转义,比如常用的目录表示,引入原始字符串后,非常方便,但是在正则(re)时,raw string literal特别有用。

格式:

R"(原始字符串)"

参考:String literal - cppreference.com

#include <iostream>
#include <string>
 
int main()
{
    std::string s1 = R"NOT_PRINT_FLAG(First Line.\nSecond Line. Also IN the FIRST Line)NOT_PRINT_FLAG";
    std::string s2 = R"(First Line.
Second Line. Is REAL Second Line.
Third Line.
End Line
)";
 
    printf("C++ Raw String is raw string\n");
    printf("Note: \\n print.\n");
    printf("      Out side '(' and ')' not print.\n");
    printf("      And If had NOT_PRINT_FLAG, they must be Same as each others.\n");
    printf("      Or Do not use NOT_PRINT_FLAG.\n");
    printf("      After ')' there Should not had any \\n or like characters.\n");
    printf("\n");
    printf("s1:\n");
    printf(s1.c_str());
    printf("\n");
    printf("\n");
    printf("s2:\n");
    printf(s2.c_str());
 
    return 0;
}
 
/*Output:
C++ Raw String is raw string
Note: \n print.
      Out side '(' and ')' not print.
      And If had NOT_PRINT_FLAG, they must be Same as each others.
      Or Do not use NOT_PRINT_FLAG.
      After ')' there Should not had any \n or like characters.
s1:
First Line.\nSecond Line. Also IN the FIRST Line
s2:
First Line.
Second Line. Is REAL Second Line.
Third Line.
End Line
*/

二、什么是multibyte/wide-character?

MultiByte是多字节字符集,Wide-Character是宽字符集.

通常所说的Unicode就是宽字符集.

Character:

其实Unicode有很多种,顾名思意,Unicode就是唯一编码的意思.也就是说对于每个字符都有与其唯一对应的编码.像常用的UTF8,UTF16 都是Unicode.最早的Unicode是UTF16的一种,所以,现在说到Unicode通常就是指UTF16.

MultiByte:

多字节字符集就是一种可变长的字符集,在这种编码中,每个字符的长度可以是一个或多个字节(其实Wide-Character只是MultiByte的一种特例).像UTF8这种就是多字节字符集.

与此相应的还有一种Single-Byte字符集,像ASCII这种就是单字节字符集.

由于MultiByte字符集对Single Byte的兼容性很好,很多程序几乎不用修改就可以正常运行,所以现在大多的多语言应用都会使用UTF8做为其字符集.

如果你想要写一个ANSI串,

char str[] = "I'm student" ;  

如果你想要写一个Unicode(Wide Character)串,那么

wchar str[] = L"I'm student" ;

那么如何判断一个(英文)字符是否是MultiByte/Wide Character?

“一个字符”?MultiByte一个字符只占一个字节,而Wide Character一个字符占2个字节(一个short)。

并不是所有的MultiByte只占一个byte,比如中文,韩文,日文


3.7 关键字noexcept

将这个关键字,首先要说一个点:异常,这一章是感情章,泪目了。

C++中的错误有很多种,例如:你代码写错了,编译器报错;你逻辑错了,导致运行结果有问题;除数为0,导致程序直接崩溃;标准库里的类和函数,使用过程中出了问题而抛出的异常。

异常的出现

异常就是错误。如果你没有设计异常,那么第一次见到异常的情况就是调用标准库的时候,先来看一个例子。

#include <iostream>
#include <string> // std::stoi()

int main(void)
{
    auto value = std::stoi("哈哈,这里不是数字");
    //std::stoi解析str,将其内容解释为指定基数的整数,该整数作为int值返回。
    //代码中的auto value = std::stoi("哈哈,这里不是数字");
    //这一条语句,std::stoi()抛出了异常。
    //当异常到了主函数时还没有被捕获,那么就会调用函数std::terminate()强制终止程序。
    //当一个地方出现异常的时候,那么如果继续运行下去的话,后面也将会是错误的,所以没有必要继续运行下去。(情感真言)
    std::cout << value << std::endl;
    return 0;
}

stoi - C++ Reference (cplusplus.com)

再来一个例子:

#include <iostream>
#include <string> // std::stoi()

int leimu(void)
{
    std::cout << "leimu函数执行开始" << std::endl;
    auto value = std::stoi("泪目了,为什么这里不写数字");
    std::cout << "leimu函数执行结束" << std::endl;
    return value;
}

int le(void)
{
    std::cout << "le函数执行开始" << std::endl;
    auto num = test();
    std::cout << "le函数执行结束" << std::endl;
    return num;
}

int main(void)
{
    std::cout << "main函数执行开始" << std::endl;
    std::cout << play() << std::endl;
    std::cout << "main函数执行结束" << std::endl;
    return 0;
}
/*output:
main函数执行开始
leimu函数执行开始
le函数执行开始
*/

上述例子告诉了我们什么道理:很多时候,开始了,但是根本不会有结束,出异常了。此处大赞!

异常是在出现的那一行直接结束函数,然后回到调用该函数的那一行,如果不捕获就又会在这一行直接结束函数,这样一层一层地抛出异常,直到回到主函数还没有捕获的话就会终止程序。这就是异常抛出的流程。


什么是异常保证呢

异常保证就是函数里在出现错误后抛出异常前的处理方法。例如std::stoll发现参数错误后,它会进行怎样的操作之后再抛出异常。异常保证就是对这个操作的说明。

常见异常保证:

//不抛出异常保证(Nothrow exception guarantee):函数决不抛出异常。
//强异常保证(Strong exception guarantee):若函数抛出异常,则程序的状态被回滚到正好在函数调用前的状态。
//基础异常保证(Basic exception guarantee):若函数抛出异常,则程序在合法状态。它可能需要清理,但所有不变量都原封不动。
//无异常保证(No exception guarantee):若函数抛出异常,则程序可能不在合法状态:可能已经发生了资源泄漏、内存谬误,或其他摧毁不变量的错误。

然而现实只有两种形式的“异常抛出保证”:

//一是操作可能抛出异常(any)

//二是操作绝不会抛出任何异常。 throw()

注意事项:

  • 在使用标准库或者别人的库的时候,需要看清楚文档。一般某个函数会抛出怎样的异常,都会在说明文档中列出来。
  • new申请堆内存时,当内存不足时也会抛出异常。虽然说以现在的操作系统技术基本上不会没有内存,但是也不排除会抛出异常。当申请堆内存抛出异常时,意味着操作系统真的不能分配内存,也意味着程序没有内存支撑它继续运行,这个时候就只能让它崩溃,或者捕获异常,简单记录崩溃信息或者其他有用的信息,然后就结束程序。如果需要捕获异常然后记录信息,不必在每次new的时候都捕获异常,这样是浪费时间的,应该在主函数里捕获。

上述的没有问题了,就真正开始了noexcept:

C++11 引入了noexcept,它有两类作用:

noexcept 指定符和noexcept 运算符

一 noexcept 指定符

1 含义:指定函数是否抛出异常。

2 noexcept两类语法:

  • noexcept(true) 等价于:指明noexcept而不带条件
  • noexcept(expression)

3.注释
C++17前:
noexcept 规定不是函数类型的一部分(正如同动态异常规定:列出函数可能直接或间接抛出的异常),而且只能在声明函数、变量、函数类型的非静态数据成员、指向函数指针、到函数的引用或指向成员函数的指针时,和声明类型正好是指向函数指针或到函数的引用的参数或返回类型时,作为 lambda 声明器或顶层函数声明器的一部分出现。它不能出现于 typedef 或类型别名声明。

void foo() noexcept; // 函数 foo() 不抛出,若有异常未在foo内处理,,如果foo()抛出异常,程序会被终止。然后std::terminate()被调用并默认调用std::abort().
void (*fp)() noexcept(false); // fp 指向可能抛出的函数
void g(void pfa() noexcept);  // g 接收指向不抛出的函数的指针
// typedef int (*pf)() noexcept; // 错误

二 noexcept 运算符

1 含义:noexcept运算符进行编译时检查,若表达式声明为不抛出任何异常则返回true。

2 语法:

noexcept( expression ) 
//return为bool类型的纯右值

3 解释:

注意:noexcept 运算符不对 expression 求值。

C++17起:若 expression 的潜在异常集合为空则结果为true,否则为false。
"潜在异常集合"的概念比较复杂,后面以修改的cppreference上的示例进行说明。

C++17前:
若 expression 含有至少一个下列潜在求值的构造则结果为 false:
调用无不抛出异常指定的任意类型函数,除非它是常量表达式;

throw 表达式;
    /*目标类型是引用类型,且转换需要运行时检查时的*/
    
dynamic_cast 表达式;
    //参数类型是多态类类型的 typeid 表达式。

所有其他情况下结果是true。


3.8 关键字constexpr

一、看到这个就想到了常量表达式

常量表达式是指值不会改变并且在编译过程中就能得到计算结果的表达式。

  • 编译过程中得到计算结果。
  • 字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。
  • 一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定。
#include<iostream>
using namespace std;

int main(int argc, char* argv[]) {
	const int max_files = 20;          //max_files是常量表达式
	const int limit = max_files + 1;   //limit是常量表达式
	int staff_size = 27;               //staff_size不是常量表达式,数据类型只是普通类型而非常量类型
	const int sz = get_size();         //重要!!!sz本身是常量,但它的具体值直到运行时才能获得,不是常量表达式
}

二、constexpr变量:

C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化:

#include<iostream>
using namespace std;

int main(int argc, char* argv[]) {
	constexpr int max_files = 20;          //20是常量表达式
	constexpr int limit = max_files + 1;   //max_files + 1是常量表达式
	constexpr int sz = size();         //重要!!!只有当size是一个constexpr函数时,才是一条正确的声明语句
}

三、constexpr函数

constexpr函数是指用于常量表达式的函数。遵循以下规定:

  • 函数的返回类型以及所有形参的类型都得是字面值类型;
  • 函数体中必须只有一条return语句。
#include<iostream>
using namespace std;

constexpr int test_sz() {  
    //无参的constexpr函数,编译器再编译时会帮我们验证test_sz()函数的返回值是否是常量表达式
    //if OK -->> 用test_sz函数初始化constexpr类型的变量coll
	return 42;
}

int main(int argc, char* argv[]) {
	constexpr int coll = test_sz();
	cout << coll << endl;  //output:42
	getchar();
	return 0;
}
  • 执行该初始化任务时,编译器把对constexpr函数的调用替换成其结果值。为了在编译过程中随时展开,constexpr函数被隐式地指定为内联函数

  • constexpr函数体内也可以含有其他语句,只要这些语句在运行时不执行任何操作就行,求解答?

    例如,constexpr函数中可以有空语句、类型别名以及using声明。

constexpr int size() {  //返回值类型为字面值类型
	; //空语句
	using In = int; //using声明
	typedef int INT; //类型别名
	return 10;
}
  • 我们允许constexpr函数的返回值并非一个常量。当scale的实参是常量表达式,它的返回值也是常量表达式,反之则不然:
#include<iostream>
using namespace std;

constexpr int size() {
	;
	using namespace std;
	typedef int INT;
	return 10;
}

constexpr int test_sz() {
	return 42;
}

//如果arg是常量表达式,则scale(arg)也是常量表达式
constexpr size_t scale(size_t cnt) {
	return test_sz()*cnt;
}

int main(int argc, char* argv[]) {
	int arr[scale(2)];  //正确
	//int i = 2;
	//int arr2[scale(i)];  //错误:scale(i)不是常量表达式
	const int j = 2;
	int arr3[scale(j)];  //正确
	getchar();
	return 0;
}

和其他函数不一样,内联函数和constexpr函数可以在程序中多次定义。毕竟,编译器要想展开函数仅有函数声明是不够的,还需要函数的定义。不过,对于某个给定的内联函数或者constexpr函数来说,它的多个定义必须完全一致。基于这个原因,内联函数和constexpr函数通常定义在头文件中。

四、指针和constexpr

在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关:

const int *p = nullptr; //p是一个指向整型常量的指针     "指针"
constexpr int *q = nullptr;  //q是一个指向整型的常量指针   "常量const"
//p和q的类型相差甚远
//p是一个指向常量的指针,q是一个常量指针,其中关键在于constexpr把它所定义的对象置为顶层const。

3.9 Template模板类

这个我是看github上一个大佬写的教学指南。

看完就一种感觉,我脑子是真的不够用,书籍我只简单地看了一下。


3.10 Lambda

此处引用微软vs2022中的阐述:

An illustration of the structural elements of a lambda expression.

  1. capture 子句 (C++ 规范中也称为 lambda 引入器 。)
  2. 参数列表 选。 (也称为 lambda 声明符)
  3. 可变规范 选。
  4. exception-specification 选。
  5. trailing-return-type 选。
  6. lambda 正文

C++中,一个lambda表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。它与普通函数不同的是,lambda必须使用尾置返回来指定返回类型。

例如调用中的std::sort,ISO C++ 98 的写法是要先写一个compare函数:

bool compare(int& a,int& b)
{
    return a>b;
}

然后,再这样调用:

sort(a, a+n, compare);

然而,用ISO C++ 11 标准新增的Lambda表达式,可以这么写:

sort(a, a+n, [](int a,int b){return a>b;});//降序排序

由于Lambda的类型是单一的,不能通过类型名来显式声明对应的对象,但可以利用auto关键字和类型推导:

auto f=[](int a,int b){return a>b;};

和其它语言的一个较明显的区别是Lambda和C++的类型系统结合使用,如:

auto f=[x](int a,int b){return a>x;};//x被捕获复制 |  by value 传递给lambda,可以读取所有数据,但是不可更改。
int x=0, y=1;
auto g=[&](int x){return ++y;};//y被捕获引用,调用g后会修改y,需要注意y的生存期 | by reference 传递给lambda,
bool(*fp)(int, int)=[](int a,int b){return a>b;};//不捕获时才可转换为函数指针

Lambda表达式可以嵌套使用。

ISO C++14支持基于类型推断的泛型lambda表达式。上面的排序代码可以这样写:

sort(a, a+n, [](const auto& a,const auto& b){return a>b;});//降序排序:不依赖a和b的具体类型

因为参数类型和函数模板参数一样可以被推导而无需和具体参数类型耦合,有利于重构代码;和使用auto声明变量的作用类似,它也允许避免书写过于复杂的参数类型。特别地,不需要显式指出参数类型使使用高阶函数变得更加容易。

lambda表达式有些部分是可以省略的,所以一个最简单的lambda表达式可以是下面这样,这段代码是可以通过编译的:

[] {}; // lambda expression

如果你想指明一个返回类型,可以使用新式C++语法:

[] () -> double {

	return  42;

}

3.11 decltype

1.什么是decltype

​ decltype是C++11新增的一个关键字,和auto的功能一样,用来在编译时期进行自动类型推导。引入decltype是因为auto并不适用于所有的自动类型推导场景,在某些特殊情况下auto用起来很不方便,甚至压根无法使用。

auto 让编译器通过初始值来进行类型推演。从而获得定义变量的类型,所以说 auto 定义的变量必须有初始值

对于内置类型的对象,使用decltype很直观,但当参数为复合类型的时候就应该注意一些使用细节问题。

auto varName=value;
decltype(exp) varName=value;
  • auto根据=右边的初始值推导出变量的类型,decltype根据exp表达式推导出变量的类型,跟=右边的value没有关系
  • auto要求变量必须初始化,这是因为auto根据变量的初始值来推导变量类型的,如果不初始化,变量的类型也就无法推导
  • 而decltype不要求,因此可以写成如下形式

decltype(exp) varName;

原则上将,exp只是一个普通的表达式,它可以是任意复杂的形式,但必须保证exp的结果是有类型的,不能是void;如exp为一个返回值为void的函数时,exp的结果也是void类型,此时会导致编译错误

1.1decltype的几种形式

int x = 0;
decltype(x) y = 1;           // y -> int
decltype(x + y) z = 0;       // z -> int
const int& i = x;
decltype(i) j = y;           // j -> const int &
const decltype(z) * p = &z;  // *p  -> const int, p  -> const int *
decltype(z) * pi = &z;       // *pi -> int      , pi -> int *
decltype(pi)* pp = &pi;      // *pp -> int *    , pp -> int * *

2.推导规则

decltype的推导规则可以简单概述如下:

  • 如果exp是一个不被括号()包围的表达式,或者是一个类成员访问表达式,或者是一个单独的变量,decltype(exp)的类型和exp一致
  • 如果exp是函数调用,则decltype(exp)的类型就和函数返回值的类型一致
  • 如果exp是一个左值,或被括号()包围,decltype(exp)的类型就是exp的引用,假设exp的类型为T,则decltype(exp)的类型为T&

规则1示例:

#include<string> 
#include<iostream>
using namespace std;
 
class A{
public:
    static int total;
    string name;
    int age;
    float scores;
}
 
int A::total=0;
 
int main()
{
int n=0;
const int &r=n;
A a;
decltype(n) x=n;    //n为Int,x被推导为Int
decltype(r) y=n;    //r为const int &,y被推导为const int &
decltype(A::total)  z=0;  ///total是类A的一个int 类型的成员变量,z被推导为int
decltype(A.name) url="www.baidu.com";//url为stringleix
return 0;
}

规则2示例:

int& func1(int ,char);//返回值为int&
int&& func2(void);//返回值为int&&
int func3(double);//返回值为int
 
const int& func4(int,int,int);//返回值为const int&
const int&& func5(void);//返回值为const int&&
 
int n=50;
decltype(func1(100,'A')) a=n;//a的类型为int&
decltype(func2()) b=0;//b的类型为int&&
decltype(func3(10.5)) c=0;//c的类型为int
 
decltype(func4(1,2,3)) x=n;//x的类型为const int&
decltype(func5()) y=0;//y的类型为const int&&

exp中调用函数时需要带上括号和参数,但这仅仅是形式,并不会真的去执行函数代码。

规则3示例:

class A{
public:
   int x;
}
 
int main()
{
const A obj;
decltype(obj.x) a=0;//a的类型为int
decltype((obj.x)) b=a;//b的类型为int&
 
int n=0,m=0;
decltype(m+n) c=0;//n+m得到一个右值,c的类型为int
decltype(n=n+m) d=c;//n=n+m得到一个左值,d的类型为int &
return 0;
}

左值:表达式执行结束后依然存在的数据,即持久性数据;右值是指那些在表达式执行结束不再存在的数据,即临时性数据。一个区分的简单方法是:对表达式取地址,如果编译器不报错就是左值,否则为右值

3.实际应用

类的静态成员可以使用auto, 对于类的非静态成员无法使用auto,如果想推导类的非静态成员的类型,只能使用decltype。

示例如下:

template<typename T>
class A
{
private :
   decltype(T.begin()) m_it;
   //typename T::iterator m_it;   //这种用法会出错
public:
void func(T& container)
{
   m_it=container.begin();
}
};
 
int main()
{
 
const vector<int> v;
A<const vector<int>> obj;
obj.func(v);
return 0;
}

3.13 Scoped enumeration 枚举类型

枚举分为不限定作用域和限定作用域。

1. 不限定作用域的枚举类型没有默认类型,前置声明时一定要指定类型。

enum 名字(可选) : 类型 { 枚举项 = 常量表达式 , 枚举项 = 常量表达式 , ... }
enum 名字 : 类型 ;
enum Animal {
  DOG,
  CAT = 100,
  HORSE = 1000
};
enum smallenum: std::int16_t {
    a,
    b,
    c
};

2.限定作用域:scoped

传统的枚举有一些问题。一个是两个不同的枚举的定义可能会产生冲突,看如下两个枚举的定义:

enum egg {Small, Medium, Large, Jumbo};

enum t_shirt {Small, Medium, Large, Xlarge};

如上存在相同的作用域和名称冲突。

c++ 11提供了一种新的枚举形式通过为枚举器提供类作用域来避免这个问题。声明形式如下:

enum class egg {Small, Medium, Large, Jumbo};

enum class t_shirt {Small, Medium, Large, Xlarge};

也可以使用关键字struct而不是class,在使用时需要使用枚举数名称来限定枚举数:

egg choice = egg::Large; // the Large enumerator of the egg enum

t_shirt Floyd = t_shirt::Large; // the Large enumerator of the t_shirt enum

既然枚举器具有类作用域,那么枚举器将来自不同的枚举定义不再有潜在的名字冲突。
c++ 11还加强了作用域枚举的类型安全性。常规的枚举在某些情况下,自动将其转换为整数类型,例如枚举被赋值于int变量或用于比较表达式,但作用域枚举没有隐式转换为整数类型:

enum egg_old {Small, Medium, Large, Jumbo}; // unscoped

enum class t_shirt {Small, Medium, Large, Xlarge}; // scoped

egg_old one = Medium; // unscoped

t_shirt rolf = t_shirt::Large; // scoped

int king = one; // implicit type conversion for unscoped

int ring = rolf; // not allowed, no implicit type conversion

if (king < Jumbo) // allowed

std::cout << "Jumbo converted to int before comparison.\n";

if (king < t_shirt::Medium) // not allowed

std::cout << "Not allowed: < not defined for scoped enum.\n";

但是如果你觉得有必要,你可以做一个显式的类型转换:

int Frodo = int(t_shirt::Small); // Frodo set to 0

枚举由某个潜在的整数类型表示,在C98下选择是依赖于实现的。因此,包含枚举的结构
可能在不同的系统上有不同的大小。c++ 11为作用域的枚举消除了这种依赖关系。默认情况下,c++ 11作用域枚举的潜在类型是int,此外,还有一种表示不同选择的语法:

// underlying type for pizza is short

enum class : short pizza {Small, Medium, Large, XLarge};

: short 说明了该枚举的潜在的类型为short,在c++ 11中,您还可以使用这个语法来表示无作用域枚举的类型,但如果不选择类型,则选择编译器生成依赖于实现。


3.12 新的函数声明语法:

typename:TypeName_百度百科 (baidu.com)

3.13基础类型明确初始化

一个明确的构造函数调用,但不给实参数,基础类型会被设定初值为0.

template <typename T>
void f()
{
	T x = T();
}
//上述函数中,初始化机制确保了"x是基础类型的,会被初始化为0".
//如果template强迫设置初值为0,其值为 zero initialized 否则就是default initialized.

补充1----C++ explicit

C++提供了关键字explicit,声明为explicit的构造函数不能在隐式转换中使用。

1)C++的类型转换分为两种,一种为隐式转换,另一种为显式转换。

2)C++中应该尽量不要使用转换,尽量使用显式转换来代替隐式转换

1隐式转换

定义:隐式转换是系统跟据程序的需要而自动转换的。

1)C++类型(char,int,float,long,double等)的隐式转换:

 //算术表达式隐式转换顺序为:

 1、char - int - long - double

 2、float - double
//1)算术表达式
int m = 10;
double n = m;//n = 10.0;隐式把m转为double类型

int m = 10;
float f = 10.0;
double d = m + f;//n = 20.0;隐式把m和f转为double类型

//2)赋值
int *p = NULL; //NULL(0)隐式转换为int*类型的空指针值

//3)函数入参
float add(float f);  
add(2); //2隐式转换为float类型

//4)函数返回值
double minus(int a, int b) 
{  
    return a - b; //返回值隐式转换为double类型
}

2)C++类对象的隐式转换:

void fun(CTest test); 

class CTest 
{ 
public: 
    CTest(int m = 0); 
} 
fun(20);//隐式转换

2显式转换

定义:显式转换也叫强制转换,是自己主动让这个类型转换成别的类型。

1)C++类型(char,int,float,long,double等)的显式转换:

int m = 5;
char c = (char)m;//显式把m转为char类型

double d = 2.0;
int i = 1;
i += static_cast<int>(d);//显式把d转换为int类型
//static_cast是一个c++运算符,功能是把一个表达式转换为某种类型,但没有运行时类型检查来保证转换的安全性。

2)C++类对象的显式转换:当类构造函数只有一个参数或除了第一个参数外其余参数都有默认值时,则此类有隐含的类型转换操作符(隐式转换),但有时隐式转换并不是我们想要的,可在构造函数前加上关键字explicit,来指定显式调用。

void fun(CTest test); 

class CTest 
{ 
public: 
    explicit CTest(int m = 0); 
} 
fun(20);//error 隐式转换
fun(static_cast<CTest>(20)); //ok 显式转换

3.10 Lambda

git

配置Git用户

个人用户根目录,依次执行如下命令:

git config --global user.name  "phaethonwb"

git config --global user.email "phaethonwb@gmail.com"

执行后,检查确认是否设置完成:

cat .gitconfig

配置提交模板

依次执行如下命令:

git config --global commit.template /home/wubo4/cmo_template

git config --global core.editor vi

执行后,检查确认是否设置完成:

cat .gitconfig
posted @ 2022-05-24 20:36  bowuwb  阅读(88)  评论(0编辑  收藏  举报
Fork me on GitHub