C++怪癖知识点

C/C++类型转换的本质

在 C/C++ 中,不同的数据类型之间可以相互转换:无需用户指明如何转换的称为自动类型转换(隐式类型转换),需要用户显式地指明如何转换的称为强制类型转换(显式类型转换),这点已在《C++转换构造函数》中进行了说明。

隐式类型转换利用的是编译器内置的转换规则,或者用户自定义的转换构造函数以及类型转换函数(这些都可以认为是已知的转换规则),例如从 int 到 double、从派生类到基类、从type *void *、从 double 到 Complex 等。

type *是一个具体类型的指针,例如 int *double *Student *等,它们都可以直接赋值给 void *指针。而反过来是不行的,必须使用强制类型转换才能将 void *转换为 type *,例如,malloc() 分配内存后返回的就是一个 void *指针,我们必须进行强制类型转换后才能赋值给指针变量。

所谓数据类型转换,就是对数据所占用的二进制位做出重新解释。如果有必要,在重新解释的同时还会修改数据,改变它的二进制位。对于隐式类型转换,编译器可以根据已知的转换规则来决定是否需要修改数据的二进制位;而对于强制类型转换,由于没有对应的转换规则,所以能做的事情仅仅是重新解释数据的二进制位,但无法对数据的二进制位做出修正。这就是隐式类型转换和强制类型转换最根本的区别。

这里说的修改数据并不是修改原有的数据,而是修改它的副本(先将原有数据拷贝到另外一个地方再修改)。

C++ auto类型推导完全攻略

auto类型推导

int  x = 0;
auto &r1  = x;
auto r2 = r1;    //r2 为  int,auto 推导为 int
  • 第 3 行代码是需要重点说明的,r1 本来是 int& 类型,但是 auto 却被推导为 int 类型,这表明当=右边的表达式是一个引用类型时,auto 会把引用抛弃,直接推导出它的原始类型。
int  x = 0;
const  auto n = x;
auto f = n;      //f 为 int,auto 被推导为 int(const 属性被抛弃)
const auto &r1 = x;
auto &r2 = r1;  //r1 为 const int& 类型,auto 被推导为 const int 类型
  • 第 3 行代码中,n 为 const int 类型,但是 auto 却被推导为 int 类型,这说明当=右边的表达式带有 const 属性时, auto 不会使用 const 属性,而是直接推导出 non-const 类型。
  • 第 5 行代码中,r1 是 const int & 类型,auto 也被推导为 const int 类型,这说明当 const 和引用结合时,auto 的推导将保留表达式的 const 类型。

最后我们来简单总结一下 auto 与 const 结合的用法:

  • 当类型不为引用时,auto 的推导结果将不保留表达式的 const 属性;
  • 当类型为引用时,auto 的推导结果将保留表达式的 const 属性。

Nested Classes

  • Nested classes are most often used to define implementation classes.
  • Nested classes are independent classes and are largely unrelated to their enclosing class. In particular, objects of the enclosing and nested classes are independent from each other. An object of the nested type does not have members defined by the enclosing class. Similarly, an object of the enclosing class does not have members defined by the nested class.
  • The enclosing class has no special access to the members of a nested class, and the nested class has no special access to members of its enclosing class.
  • A nested class defines a type member in its enclosing class. As with any other member, the enclosing class determines access to this type. A nested class defined in the public part of the enclosing class defines a type that may be used anywhere. A nested class defined in the protected section defines a type that is accessible only by the enclosing class, its friends, and its derived classes. A private nested class defines a type that is accessible only to the members and friends of the enclosing class.
  • Until the actual definition of a nested class that is defined outside the class body is seen, that class is an incomplete type.
  • Although a nested class is defined in the scope of its enclosing class, it is important to understand that there is no connection between the objects of an enclosing class and objects of its nested classe(s). A nested-type object contains only the members defined inside the nested type. Similarly, an object of the enclosing class has only those members that are defined by the enclosing class. It does not contain the data members of any nested classes.

thread_local:

Storage-class specifiers

thread_local变量

Thread-Local Data

C++11右值引用

右值引用指的是以引用传递(而非值传递)的方式使用 C++ 右值。

lvalue 是“loactor value”的缩写,可意为存储在内存中、有明确存储地址(可寻址)的数据,而 rvalue 译为 "read value",指的是那些可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据)。

有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。

字面量通常存储在寄存器中,或者和代码存储在一起。

虽然 C++98/03 标准不支持为右值建立非常量左值引用,但允许使用常量左值引用操作右值。

int &c = 10;          //错误
const int &c = 10;    //正确

实际开发中我们可能需要对右值进行修改(实现移动语义时就需要)。

和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化,比如:

int num = 10;
//int && a = num;  //右值引用不能初始化为左值
int && a = 10;

和常量左值引用不同的是,右值引用还可以对右值进行修改。例如:

int && a = 10;
a = 100;
cout << a << endl;

程序输出结果为 100。

C++ 语法上是支持定义常量右值引用的,例如:

const int&& a = 10;//编译器不会报错

但这种定义出来的右值引用并无实际用处。一方面,右值引用主要用于移动语义和完美转发,其中前者需要有修改右值的权限;其次,常量右值引用的作用就是引用一个不可修改的右值,这项工作完全可以交给常量左值引用完成。

C++左值引用和右值引用
引用类型可以引用的值类型使用场景
非常量左值常量左值非常量右值常量右值
非常量左值引用

int i = 10;
int& r = i;

Y

const int i = 10;
int& r = i;

N

demo& r = get_demo();

Y

虽然 C++98/03 标准不支持为右值建立非常量左值引用,

demo& r = const get_demo();

N

常量左值引用

int i = 10;
const int& r = i;

Y

const int i = 10;
const int& r = i;

Y

const demo& r = get_demo();

Y

但允许使用常量左值引用操作右值。

const demo& r = const get_demo();

Y

常用于类中构建拷贝构造函数
非常量右值引用

int i = 10;
int&& r = i;

N

const int i = 10;
int&& r = i;

N

demo&& r = get_demo();

Y

demo&& r = const get_demo();

N

移动语义、完美转发
常量右值引用

int i = 10;
const int&& r = i;

N

const int i = 10;
const int&& r = i;

N

const demo&& r = get_demo();

Y

const demo&& r = const get_demo();

Y

无实际用途

表中,Y 表示支持,N 表示不支持。红色字体表示自己的错误理解。黄色字体表示正确观点,但自己不知道。

Q:什么是非常量右值,请举例说明?

程序执行结果中产生的非 const临时对象(例如函数返回值、lambda 表达式等)既无名称也无法获取其存储地址,所以属于非常量右值。

class demo
{
};

demo get_demo()
{
    return demo();
}

int main()
{
    demo a = get_demo();
    return 0;
}

get_demo返回的就是非常量右值。

C++11移动构造函数的功能和用法

借助右值引用可以实现移动语义,解决深拷贝导致的效率问题。

移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象,将其他对象(通常是匿名/临时对象)包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。

当使用(函数返回的)临时对象初始化目的对象时,只需要将临时对象的指针直接浅拷贝给目的对象的指针,然后修改该临时对象中指针的指向(通常令其指向 NULL),这样就完成了目的对象指针的初始化。

class demo
{
public:
    // 移动构造函数
    demo(demo &&d):num(d.num){
        d.num = nullptr;
        cout<<"move construct!"<<endl;
    }
    
private:
    int *num;
};

移动构造函数使用右值引用形式的参数,指针变量采用的是浅拷贝的复制方式,同时在函数内部重置入参对象的指针为nullptr,有效避免了“同一块空间被释放多次”情况的发生。

非 const 右值引用只能操作右值,程序执行结果中产生的临时对象(例如函数返回值、lambda 表达式等)既无名称也无法获取其存储地址,所以属于右值。当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。

通常在类中自定义移动构造函数的同时,会再为其自定义一个适当的拷贝构造函数,由此当用户利用右值初始化类对象时,会调用移动构造函数;使用左值(非右值)初始化类对象时,会调用拷贝构造函数。

想调用移动构造函数,则必须使用右值进行初始化。

左值初始化同类对象只能通过拷贝构造函数完成,为了满足用户使用左值初始化同类对象时也通过移动构造函数完成的需求,新引入了 std::move() 函数,它可以将左值强制转换成对应的右值,由此便可以使用移动构造函数。

C++11 move()函数:将左值强制转换为右值

move 本意为 "移动",但该函数并不能移动任何数据,只是将某个左值强制转化为右值。

基于 move() 函数特殊的功能,其常用于实现移动语义。

move() 函数的语法格式如下:

move( arg )

其中,arg 表示指定的左值对象。该函数会返回 arg 对象的右值形式。

【例 1】move() 函数的基础应用。

#include <iostream>
using namespace std;

class movedemo{
public:
    movedemo():num(new int(0)){
        cout<<"construct!"<<endl;
    }
    //拷贝构造函数
    movedemo(const movedemo &d):num(new int(*d.num)){
        cout<<"copy construct!"<<endl;
    }
    //移动构造函数
    movedemo(movedemo &&d):num(d.num){
        d.num = NULL;
        cout<<"move construct!"<<endl;
    }
public:     //这里应该是 private,使用 public 是为了更方便说明问题
    int *num;
};

int main(){
    movedemo demo;
    cout << "demo2:\n";
    movedemo demo2 = demo;
    //cout << *demo2.num << endl;   //可以执行
    cout << "demo3:\n";
    movedemo demo3 = std::move(demo);
    //此时 demo.num = NULL,因此下面代码会报运行时错误
    //cout << *demo.num << endl;
    return 0;
}

程序执行结果为:

construct!
demo2:
copy construct!
demo3:
move construct!

通过观察程序的输出结果,以及对比 demo2 和 demo3 初始化操作不难得知,demo 对象作为左值,直接用于初始化 demo2 对象,其底层调用的是拷贝构造函数;而通过调用 move() 函数可以得到 demo 对象的右值形式,用其初始化 demo3 对象,编译器会优先调用移动构造函数。

注意,调用拷贝构造函数,并不影响 demo 对象,但如果调用移动构造函数,由于函数内部会重置 demo.num 指针的指向为 NULL,所以程序中第 30 行代码会导致程序运行时发生错误。

C++11引用限定符的用法 

引用限定符用于限制调用成员函数的对象的类型(左值还是右值)。

#include <iostream>
using namespace std;

class demo {
public:
    demo(int num):num(num){}
    // "&"限定调用该函数的对象必须是左值对象。
    int get_num()&
    {
        return this->num;
    }
private:
    int num;
};
int main() {
    demo a(10);
    cout << a.get_num() << endl;          // 正确
    // 错误,move(a) 生成的右值对象是不允许调用 get_num() 函数的。
    //cout << move(a).get_num() << endl;
    return 0;
}
#include <iostream>
using namespace std;

class demo {
public:
    demo(int num):num(num){}
    // "&&" 限定调用该函数的对象必须是一个右值对象。
    int get_num()&&{
        return this->num;
    }
private:
    int num;
};
int main() {
    demo a(10);
    //cout << a.get_num() << endl;      // 错误
    cout << move(a).get_num() << endl;  // 正确
    return 0;
}

引用限定符不适用于静态成员函数和友元函数。

const和引用限定符

当引用限定符和 const 修饰同一个类的成员函数时,const 必须位于引用限定符前面。

当 const && 修饰类的成员函数时,调用它的对象只能是右值对象;当 const & 修饰类的成员函数时,调用它的对象既可以是左值对象,也可以是右值对象。

C++11完美转发及其实现

完美转发指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。

举个例子:

template<typename T>
void function(T t) {
    otherdef(t);
}

如上所示,function() 函数模板中调用了 otherdef() 函数。在此基础上,完美转发指的是:如果 function() 函数接收到的参数 t 为左值,那么该函数传递给 otherdef() 的参数 t 也是左值;反之如果 function() 函数接收到的参数 t 为右值,那么传递给 otherdef() 函数的参数 t 也必须为右值。

显然,function() 函数模板并没有实现完美转发。一方面,参数 t 为非引用类型,这意味着在调用 function() 函数时,实参将值传递给形参的过程就需要额外进行一次拷贝操作;另一方面,无论调用 function() 函数模板时传递给参数 t 的是左值还是右值,对于函数内部的参数 t 来说,它有自己的名称,也可以获取它的存储地址,因此它永远都是左值,也就是说,传递给 otherdef() 函数的参数 t 永远都是左值。总之,无论从那个角度看,function() 函数的定义都不“完美”。

是否实现完美转发,直接决定了该参数的传递过程使用的是拷贝语义(调用拷贝构造函数)还是移动语义(调用移动构造函数)。

使用非 const 引用作为函数模板参数时,只能接收左值,无法接收右值;而 const 左值引用既可以接收左值,也可以接收右值,但考虑到其 const 属性,除非被调用函数的参数也是 const 属性,否则将无法直接传递。

通常情况下右值引用形式的参数只能接收右值,不能接收左值。但对于函数模板中使用右值引用语法定义的参数来说,它不再遵守这一规定,既可以接收右值,也可以接收左值(此时的右值引用又被称为“万能引用”)。

实现完美转发,只需要编写如下一个模板函数即可:

template <typename T>
void function(T&& t) {
    otherdef(t);
}

此模板函数的参数 t 既可以接收左值,也可以接收右值。如果调用 function() 函数时为其传递一个左值引用或者右值引用的实参,如下所示:

int n = 10;
int & num = n;
function(num); // T 为 int&

int && num2 = 11;
function(num2); // T 为 int &&

其中,由 function(num) 实例化的函数底层就变成了 function(int & & t),同样由 function(num2) 实例化的函数底层则变成了 function(int && && t)。为了更好地实现完美转发,特意为其指定了引用折叠规则(假设用 A 表示实际传递参数的类型):

  • 当实参为左值或者左值引用(A&)时,函数模板中 T&& 将转变为 A&(A& && = A&);
  • 当实参为右值或者右值引用(A&&)时,函数模板中 T&& 将转变为 A&&(A&& && = A&&)。

读者只需要知道,在实现完美转发时,只要函数模板的参数类型为 T&&,则 C++ 可以自行准确地判定出实际传入的实参是左值还是右值。

通过将函数模板的形参类型设置为 T&&,我们可以很好地解决接收左、右值的问题。模板函数 forword<T>()解决将函数模板接收到的实参连同其左、右值属性,一起传递给被调用的函数的问题。

#include <iostream>
using namespace std;

//重载被调用函数,查看完美转发的效果
void otherdef(int & t) {
    cout << "lvalue\n";
}
void otherdef(const int & t) {
    cout << "rvalue\n";
}

//实现完美转发的函数模板的最终版本
template <typename T>
void function(T&& t) {
    otherdef(forward<T>(t));
}

int main()
{
    function(5);
    int  x = 1;
    function(x);
    return 0;
}

程序执行结果为:

rvalue
lvalue

forword() 函数模板用于修饰被调用函数中需要维持参数左、右值属性的参数。

总的来说,在定义模板函数时,我们采用右值引用的语法格式定义参数类型,由此该函数既可以接收外界传入的左值,也可以接收右值;其次,还需要使用 C++11 标准库提供的 forword() 模板函数修饰被调用函数中需要维持左、右值属性的参数。由此即可轻松实现函数模板中参数的完美转发。 

C++11 lambda匿名函数用法详解

lambda匿名函数的定义

定义 lambda 匿名函数语法格式:

[外部变量访问方式说明符] (参数) mutable noexcept/throw() -> 返回值类型
{
   函数体;
};

C++11 for循环(基于范围的循环)详解

用 C++ 11 标准中的 for 循环遍历实例一定义的 arc 数组

#include <iostream>
using namespace std;

int main() {
    char arc[] = "http://c.biancheng.net/cplus/11/";
    //for循环遍历普通数组
    for (char ch : arc) {
        cout << ch;
    }
    cout << '!' << endl;

    return 0;
}

程序执行结果为:

http://c.biancheng.net/cplus/11/!

新格式的 for 循环在遍历字符串序列时,只遍历到最后一个字符,不会遍历位于该字符串末尾的 '\0'(字符串的结束标志)。

在使用新语法格式的 for 循环遍历某个序列时,如果需要遍历的同时修改序列中元素的值,实现方案是在 declaration 参数处定义引用形式的变量。举个例子:

#include <iostream>
#include <vector>
using namespace std;

int main() {
    char arc[] = "abcde";
    vector<char>myvector(arc, arc + 5);
    //for循环遍历并修改容器中各个字符的值
    for (auto &ch : myvector) {
        ch++;
    }
    //for循环遍历输出容器中各个字符
    for (auto ch : myvector) {
        cout << ch;
    }
    cout << endl;
    return 0;
}

程序执行结果为:

bcdef

如果需要在遍历序列的过程中修改器内部元素的值,定义declaration 参数时就必须定义引用形式的变量;反之,建议定义const &(常引用)形式的变量(避免了底层复制变量的过程,效率更高)。

C++11 for循环使用注意事项

#include <iostream>
#include <vector>
using namespace std;

int main() {
    char arc[] = "http://c.biancheng.net/cplus/11/";
    //for循环遍历 vector 容器
    vector<char>myvector(arc, arc + 23);
    for (auto ch : myvector) {
        cout << ch;
    }
	cout << endl;
    return 0;
}

程序执行结果为:

http://c.biancheng.net/

对于用基于范围的 for 循环遍历容器中的元素,很多读者会将 ch 误认为指向各个元素的迭代器,其实不然,它表示的仍是容器中的各个元素。

基于范围的 for 循环也可以遍历 string 类型的字符串,这种情况下冒号前定义 char 类型的变量即可。

基于范围的 for 循环可以遍历普通数组、string字符串、容器以及初始化列表。除此之外,for 循环冒号后还可以放置返回 string 字符串以及容器对象的函数,比如:

#include <iostream>
#include <vector>
#include <string>
using namespace std;

string str = "http://c.biancheng.net/cplus/11/";
vector<int> myvector = { 1,2,3,4,5 };

string retStr() {
    return str;
}

vector<int> retVector() {
    return myvector;
}

int main() {
    //遍历函数返回的 string 字符串
    for (char ch : retStr()) {
        cout << ch;
    }
    cout << endl;
    //遍历函数返回的 vector 容器
    for (int num : retVector()) {
        cout << num << " ";
    }
    cout << endl;
    return 0;
}

程序执行结果为:

http://c.biancheng.net/cplus/11/
1 2 3 4 5

基于范围的 for 循环不支持遍历函数返回的以指针形式表示的数组,比如:

//错误示例
#include <iostream>
using namespace std;

char str[] = "http://c.biancheng.net/cplus/11/";

char* retStr() {
    return str;
}

int main() {
    for (char ch : retStr()) //直接报错
    {
        cout << ch;
    }
    return 0;
}

原因很简单,此格式的 for 循环只能遍历有明确范围的一组数据,上面程序中 retStr() 函数返回的是指针变量,遍历范围并未明确指明,所以编译失败。

当基于范围的 for 循环遍历的是某函数返回的 string 对象或者容器时,整个遍历过程中,函数只会执行一次。

举个例子:

#include <iostream>
#include <string>
using namespace std;

string str= "http://c.biancheng.net/cplus/11/";

string retStr() {
    cout << "retStr:" << endl;
    return str;
}

int main() {
    //遍历函数返回的 string 字符串
    for (char ch : retStr()) {
        cout << ch;
    }
	cout << endl;
    return 0;
}

程序执行结果为:

retStr:
http://c.biancheng.net/cplus/11/

借助执行结果不难分析出,整个 for 循环遍历 str 字符串对象的过程中,retStr() 函数仅在遍历开始前执行了 1 次。

系统学过 STL 标准库的读者应该知道,基于关联式容器(包括哈希容器)底层存储机制的限制:

  1. 不允许修改 map、unordered_map、multimap 以及 unordered_multimap 容器存储的键的值;
  2. 不允许修改 set、unordered_set、multiset 以及 unordered_multiset 容器中存储的元素的值。

因此,当使用基于范围的 for 循环遍历此类型容器时,切勿修改容器中不允许被修改的数据部分,否则会导致程序的执行出现各种 Bug。

另外,基于范围的 for 循环完成对容器的遍历,其底层也是借助容器的迭代器实现的。举个例子:

#include <iostream>
#include <vector>
int main(void)
{
    std::vector<int>arr = { 1, 2, 3, 4, 5 };
    for (auto val : arr)
    {
        std::cout << val << std::endl;
        arr.push_back(10); //向容器中添加元素
    }
    std::cout << std::endl;
    return 0;
}

程序执行结果可能为(输出结果不唯一):

1
0
379097104
21870
5

可以看到,程序的执行结果并不是我们想要的。就是因为在 for 循环遍历 arr 容器的同时向该容器尾部添加了新的元素(对 arr 容器进行了扩增),致使遍历容器所使用的迭代器失效,整个遍历过程出现错误。

因此,在使用基于范围的 for 循环遍历容器时,应避免在循环体中修改容器存储元素的个数。

C++11/C++14 New Features

C++11/C++14新特性

什么是 JSON?

JavaScript Object Notation (JSON) is a standard text-based format for representing structured data based on JavaScript object syntax. 

JSON is a text-based data format following JavaScript object syntax.

Even though it closely resembles JavaScript object literal syntax, it can be used independently from JavaScript, and many programming environments feature the ability to read (parse) and generate JSON.

JSON exists as a string — useful when you want to transmit data across a network. It needs to be converted to a native JavaScript object when you want to access the data.

Note: Converting a native object to a string so it can be transmitted across the network is called serialization, while converting a string to a native object is called deserialization.

JSON is a string whose format very much resembles JavaScript object literal format.

JSON text basically looks like a JavaScript object inside a string.

JSON的作用是什么?

It is commonly used for transmitting data in web applications (e.g., sending some data from the server to the client, so it can be displayed on a web page, or vice versa). 

在JavaScript和C++中,如何实现JSON string和native object间的转换?

sometimes we receive a raw JSON string from network , and we need to convert it to an object ourselves.

  • JSON.parse(): Accepts a JSON string as a parameter, and returns the corresponding JavaScript object.

And when we want to send a JavaScript object across the network, we need to convert it to JSON (a string) before sending.

  • JSON.stringify(): Accepts an object as a parameter, and returns the equivalent JSON string.

(JSON object、JSON value)在JavaScript中的表示:

const person = {
    name : {
        first: 'Bob',
        last: 'Smith'
    },
    age: 32,
    gender: 'male',
    interests: ['music', 'skiing'],
    bio: function() {
        alert(this.name[0] + ' ' + this.name[1] + ' is ' + this.age + ' years old. He likes ' + this.interests[0] + ' and ' + this.interests[1] + '.');
    },
    greeting: function() {
        alert('Hi! I\'m ' + this.name[0] + '.');
    }
};

(JSON object、JSON value)在C++中的表示:

json json_types =
{
    {"boolean", true},
    {
        "number", {
            {"integer", 42},
            {"floating-point", 17.23}
        }
    },
    {"string", "Hello, world!"},
    {"array", {1, 2, 3, 4, 5}},
    {"null", nullptr}
};

JSON structure:

{
    "name": {
        "first": "Bob",
        "last": "Smith"
    },
    "age": 32,
    "gender": "male",
    "interests": ["music", "skiing"]
}

Other notes

  • JSON is purely a string with a specified data format — it contains only properties, no methods.
  • JSON requires double quotes to be used around strings and property names. Single quotes are not valid other than surrounding the entire JSON string.
  • JSON can actually take the form of any data type that is valid for inclusion inside JSON, not just arrays or objects. So for example, a single string or number would be valid JSON.
  • Unlike in JavaScript code in which object properties may be unquoted, in JSON only quoted strings may be used as properties.

Strongly typed enums - enum classes

C++ 函数调用运算符 () 重载

深度理解do{} while(0)语句的作用

有符号数与无符号数的混合运算

整型提升:

  1. 在表达式运算(包括逻辑运算和算术运算)时,首先比int小的各种整型(包括char和unsigned char、short和unsigned short)都要提升为int类型;
  2. 此时如果有unsigned int类型也参与了运算,使得int类型不足以表示则要再进一步提升int类型为unsigned int类型,即会将有符号数看成无符号数,然后执行表达式的运算,返回无符号数结果。

至于第1步为什么选择转换为int,应该是从效率上考虑的,因为通常情况下int的长度被定义为机器处理效率最高的长度,比如32位机上,一次处理4个字节效率是最高的,所以虽然short占2个字节更节省内存,但是在运算中的效率,是int更高。

// 列举有符号数与无符号数混合运算可能产生与预期结果不符的全部场合。
#include <stdio.h>

int main()
{
	unsigned char uc1 = 1;
	unsigned char uc2 = 2;
	char c1 = -1;
	char c2 = -2;
 	
	unsigned int ui1 = 1;
	unsigned int ui2 = 2;
	int i1 = -1;
	int i2 = -2;
	
    /*
	 uint与signed类型的负数比较时,uint是正数本应该大于signed类型的负数,
	 但由于signed类型的负数提升为uint后也是一个正数(>=0x80000000),
	 可能使得uint类型的正数小于signed类型提升后的这个正数,导致比较结果与预期不符。
    */
	printf("ui1 > i2 ? : %s\n", ui1 > i2 ? "true" : "false");
	printf("ui2 > i1 ? : %s\n", ui2 > i1 ? "true" : "false");
	printf("ui1 > c2 ? : %s\n", ui1 > c2 ? "true" : "false");
	printf("ui2 > c1 ? : %s\n", ui2 > c1 ? "true" : "false");
	
	printf("\n");
	
	/* 
     uint与任意其他类型的整型做和/差,结果为负的情况下,将结果与0或正数比较时,本应该小于0或正数,
     但由于负数结果被编译器理解成uint类型,是一个正数,
     可能使得被编译器理解成uint类型的正数大于0或正数,导致比较结果与预期不符。
    */    
	printf("ui1 +  i2"); (ui1 +  i2 <   0) ? printf(" < 0\n") : printf(" > 0\n");
	printf("ui1 +  c2"); (ui1 +  c2 <   0) ? printf(" < 0\n") : printf(" > 0\n");
	printf("uc1 - ui2"); (uc1 - ui2 <   0) ? printf(" < 0\n") : printf(" > 0\n");
	printf("ui1 - uc2"); (ui1 - uc2 <   0) ? printf(" < 0\n") : printf(" > 0\n");
	printf(" c1 - ui1"); ( c1 - ui1 <   0) ? printf(" < 0\n") : printf(" > 0\n");
	printf(" c1 - ui2"); ( c1 - ui2 <   0) ? printf(" < 0\n") : printf(" > 0\n");
	printf(" c2 - ui1"); ( c2 - ui1 <   0) ? printf(" < 0\n") : printf(" > 0\n");
	printf(" c2 - ui2"); ( c2 - ui2 <   0) ? printf(" < 0\n") : printf(" > 0\n");
	printf("ui1 - ui2"); (ui1 - ui2 <   0) ? printf(" < 0\n") : printf(" > 0\n");
	printf(" i1 - ui1"); ( i1 - ui1 <   0) ? printf(" < 0\n") : printf(" > 0\n");
	printf(" i2 - ui1"); ( i2 - ui1 <   0) ? printf(" < 0\n") : printf(" > 0\n");
	printf(" i1 - ui2"); ( i1 - ui2 <   0) ? printf(" < 0\n") : printf(" > 0\n");
	printf(" i2 - ui2"); ( i2 - ui2 <   0) ? printf(" < 0\n") : printf(" > 0\n");
	printf(" c1 - ui1"); ( c1 - ui1 < uc1) ? printf(" < 0\n") : printf(" > 0\n");
	printf(" c1 - ui1"); ( c1 - ui1 < ui1) ? printf(" < 0\n") : printf(" > 0\n");
	printf(" i1 - ui1"); ( i1 - ui1 < uc1) ? printf(" < 0\n") : printf(" > 0\n");
	printf(" i1 - ui1"); ( i1 - ui1 < ui1) ? printf(" < 0\n") : printf(" > 0\n");
	
	printf("\n");
	
    /*
	 注意,此时运算结果的值与预期其实是相符的,
	 只是应该理解成负数,但编译器却因类型提升成uint而错误地理解成了正数。
    */
	printf("ui1 +  i2 = %d %#x\n", ui1 +  i2, ui1 +  i2);
	printf("ui1 +  c2 = %d %#x\n", ui1 +  c2, ui1 +  c2);
	printf("uc1 - ui2 = %d %#x\n", uc1 - ui2, uc1 - ui2);
	printf("ui1 - uc2 = %d %#x\n", ui1 - uc2, ui1 - uc2);
	printf(" c1 - ui1 = %d %#x\n",  c1 - ui1,  c1 - ui1);
	printf(" c1 - ui2 = %d %#x\n",  c1 - ui2,  c1 - ui2);
	printf(" c2 - ui1 = %d %#x\n",  c2 - ui1,  c2 - ui1);
	printf(" c2 - ui2 = %d %#x\n",  c2 - ui2,  c2 - ui2);
	printf("ui1 - ui2 = %d %#x\n", ui1 - ui2, ui1 - ui2);
	printf(" i1 - ui1 = %d %#x\n",  i1 - ui1,  i1 - ui1);
	printf(" i2 - ui1 = %d %#x\n",  i2 - ui1,  i2 - ui1);
	printf(" i1 - ui2 = %d %#x\n",  i1 - ui2,  i1 - ui2);
	printf(" i2 - ui2 = %d %#x\n",  i2 - ui2,  i2 - ui2);
	
	printf("\n");
	
	// char和shor参与运算时,必须先转换成int类型。
	printf("sizeof(uc1 + c2) = %d\n", sizeof(uc1 + c2));
	printf("sizeof(uc2 + c1) = %d\n", sizeof(uc2 + c1));
	
	printf("\n");
	
	int i3 = 0x7FFFFFFF;
	unsigned int ui3 = 0xFFFFFFFF;
	
	// int向上溢出导致正数变负数,uint向上溢出导致高位截断。
	printf(" i3 + 2"); (i3 + 2 > 0) ? printf(" > 0\n") : printf(" < 0\n");
	printf(" i3 + 2 = %d\t%#x\n",       i3 + 2,  i3 + 2);
	printf("ui3 + 2 = %.11d\t%#.8x\n", ui3 + 2, ui3 + 2);
	
	printf("\n");
	
	int i4 = 0x80000000;
	unsigned int ui4 = 0x01;
	
	// int/uint向下溢出导致负数变正数。
	printf(" i4 - 2"); ( i4 - 2 > 0) ? printf(" > 0\n") : printf(" < 0\n");
	printf("ui4 - 2"); (ui4 - 2 > 0) ? printf(" > 0\n") : printf(" < 0\n");
	
	printf(" i4 - 2 = %d\t%#x\n",    i4 - 2,  i4 - 2);
	printf("ui4 - 2 = %.9d\t%#x\n", ui4 - 2, ui4 - 2);
	
	return 0;
}

执行结果:

ui1 > i2 ? : false
ui2 > i1 ? : false
ui1 > c2 ? : false
ui2 > c1 ? : false

ui1 +  i2 > 0
ui1 +  c2 > 0
uc1 - ui2 > 0
ui1 - uc2 > 0
 c1 - ui1 > 0
 c1 - ui2 > 0
 c2 - ui1 > 0
 c2 - ui2 > 0
ui1 - ui2 > 0
 i1 - ui1 > 0
 i2 - ui1 > 0
 i1 - ui2 > 0
 i2 - ui2 > 0
 c1 - ui1 > 0
 c1 - ui1 > 0
 i1 - ui1 > 0
 i1 - ui1 > 0

ui1 +  i2 = -1 0xffffffff
ui1 +  c2 = -1 0xffffffff
uc1 - ui2 = -1 0xffffffff
ui1 - uc2 = -1 0xffffffff
 c1 - ui1 = -2 0xfffffffe
 c1 - ui2 = -3 0xfffffffd
 c2 - ui1 = -3 0xfffffffd
 c2 - ui2 = -4 0xfffffffc
ui1 - ui2 = -1 0xffffffff
 i1 - ui1 = -2 0xfffffffe
 i2 - ui1 = -3 0xfffffffd
 i1 - ui2 = -3 0xfffffffd
 i2 - ui2 = -4 0xfffffffc

sizeof(uc1 + c2) = 4
sizeof(uc2 + c1) = 4

 i3 + 2 < 0
 i3 + 2 = -2147483647	0x80000001
ui3 + 2 = 00000000001	0x00000001

 i4 - 2 > 0
ui4 - 2 > 0
 i4 - 2 = 2147483646	0x7ffffffe
ui4 - 2 = -000000001	0xffffffff

总结:有符号数与无符号数混合运算可能产生与预期结果不符的场合的特征(三项必须同时具备):

  • uint类型参与运算;
  • 运算参数存在负数,并提升为uint类型;
  • 进行比较运算。

终止线程执行(3种方法)

线程执行过程中,接收到其它线程发送的“终止执行”的信号(pthread_cancel()),然后终止执行。

void pthread_exit(void *retval);

注意,retval 指针不能指向函数内部的局部数据(比如局部变量)。换句话说,pthread_exit() 函数不能返回一个指向局部数据的指针,否则很可能使程序运行结果出错甚至崩溃。

pthread_exit()和return的区别

首先,return 语句和 pthread_exit() 函数的含义不同,return 的含义是返回,它不仅可以用于线程执行的函数,普通函数也可以使用;pthread_exit() 函数的含义是线程退出,它专门用于结束某个线程的执行。

主线程最后执行的 return 语句不仅会终止主线程执行,还会终止其它子线程执行。

pthread_exit() 函数只会终止当前线程,不会影响其它线程的执行。

此外,pthread_exit() 函数还会自动调用线程清理程序(本质是一个由 pthread_cleanup_push() 指定的自定义函数),而 return 不具备这个能力。

终止主线程时,return 和 pthread_exit() 函数发挥的功能不同,可以根据需要自行选择。

注意,pthread_cancel() 函数的功能仅仅是向目标线程发送 Cancel 信号,至于目标线程是否接收该信号,何时响应该信号,全由目标线程决定。对于默认属性的线程,当有线程借助 pthread_cancel() 函数向它发送 Cancel 信号时,它并不会立即结束执行,而是选择在一个适当的时机结束执行。所谓适当的时机,POSIX 标准中规定,当线程执行一些特殊的函数时,会响应 Cancel 信号并终止执行,比如常见的 pthread_join()、pthread_testcancel()、sleep()、system()、pthread_cond_wait()、 pthread_cond_timedwait()等,POSIX 标准称此类函数为“cancellation points”(中文可译为“取消点”)。此外,<pthread.h> 头文件还提供有 pthread_setcancelstate() 和 pthread_setcanceltype() 这两个函数,我们可以手动修改目标线程处理 Cancel 信号的方式。

对于接收 Cancel 信号后结束执行的目标线程,等同于该线程自己执行如下语句:

pthread_exit(PTHREAD_CANCELED);

也就是说,当一个线程被强制终止执行时,它会返回PTHREAD_CANCELED这个宏(定义在<pthread.h>头文件中)。

一个线程执行结束的返回值只能由一个 pthread_join() 函数获取,当有多个线程调用 pthread_join() 函数获取同一个线程的执行结果时,哪个线程最先执行 pthread_join() 函数,执行结果就由那个线程获得,其它线程的 pthread_join() 函数都将执行失败。

对于一个默认属性的线程 A 来说,线程占用的资源并不会因为执行结束而得到释放。而通过在其它线程中执行pthread_join(A,NULL);语句,可以轻松实现“及时释放线程 A 所占资源”的目的。 默认属性的线程在执行完目标函数后,占用的私有资源并不会立即释放,要么执行完 pthread_join() 函数后释放,要么整个进程执行结束后释放。

信号量又称“信号灯”,主要用于控制同时访问公共资源的线程数量,当线程数量控制在 ≤1 时,该信号量又称二元信号量,功能和互斥锁非常类似;当线程数量控制在 N(≥2)个时,该信号量又称多元信号量,指的是同一时刻最多只能有 N 个线程访问该资源。

条件变量的功能类似于实际生活中的门,门有“打开”和“关闭”两种状态,分别对应条件变量的“成立”状态和“不成立”状态。当条件变量“不成立”时,任何线程都无法访问资源,只能等待条件变量成立;一旦条件变量成立,所有等待的线程都会恢复执行,访问目标资源。为了防止各个线程竞争资源,条件变量总是和互斥锁搭配使用。

//1、使用特定的宏
pthread_mutex_t myMutex = PTHREAD_MUTEX_INITIALIZER;
//2、调用初始化的函数
pthread_mutex_t myMutex;
pthread_mutex_init(&myMutex , NULL);

对于调用 malloc() 函数分配动态内存的互斥锁,只能以第 2 种方法完成初始化;

注意,不能对一个已经初始化过的互斥锁再进行初始化操作,否则会导致程序出现无法预料的错误。

对于使用动态内存创建的互斥锁,例如:

pthread_mutex_t *myMutex = (pthread_mutex_t *)malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(myMutex , NULL);

手动释放 myMutex 占用的内存(调用 free() 函数)之前,必须先调用 pthread_mutex_destory() 函数销毁该对象。注意,对于用 PTHREAD_MUTEX_INITIALIZER 或者 pthread_mutex_init() 函数直接初始化的互斥锁,无需调用 pthread_mutex_destory() 函数手动销毁。

和互斥锁类似,信号量本质也是一个全局变量。不同之处在于,互斥锁的值只有 2 个(加锁 "lock" 和解锁 "unlock"),而信号量的值可以根据实际场景的需要自行设置(取值范围为 ≥0)。更重要的是,信号量还支持做“加 1”或者 “减 1”运算,且修改值的过程以“原子操作”的方式实现。

多线程程序中,使用信号量需遵守以下几条规则:

  1. 信号量的值不能小于 0;
  2. 有线程访问资源时,信号量执行“减 1”操作,访问完成后再执行“加 1”操作;
  3. 当信号量的值为 0 时,想访问资源的线程必须等待,直至信号量的值大于 0,等待的线程才能开始访问。

当进程中存在多个线程,但某公共资源允许同时访问的线程数量是有限的(出现了“狼多肉少”的情况),这时就可以用计数信号量来限制同时访问资源的线程数量

假设一个进程中包含多个线程,这些线程共享变量 x,我们希望某个(或某些)线程等待 "x==10' 条件成立后再执行后续的代码,该如何实现呢?针对类似的场景,我们推荐您用条件变量来实现。和互斥锁、信号量类似,条件变量本质也是一个全局变量,它的功能是阻塞线程,直至接收到“条件成立”的信号后,被阻塞的线程才能继续执行。

一个条件变量可以阻塞多个线程,这些线程会组成一个等待队列。当条件成立时,条件变量可以解除线程的“被阻塞状态”。也就是说,条件变量可以完成以下两项操作:

  • 阻塞线程,直至接收到“条件成立”的信号;
  • 向等待队列中的一个或所有线程发送“条件成立”的信号,解除它们的“被阻塞”状态。

为了避免多线程之间发生“抢夺资源”的问题,条件变量在使用过程中必须和一个互斥锁搭配使用。

pthread_cond_wait和pthread_cond_timedwait会完成以下两项工作:

  • 阻塞线程,直至接收到“条件成立”的信号;
  • 当线程被添加到等待队列上时,将互斥锁“解锁”。

也就是说,函数尚未接收到“条件成立”的信号之前,它将一直阻塞线程执行。注意,当函数接收到“条件成立”的信号后,它并不会立即结束对线程的阻塞,而是先完成对互斥锁的“加锁”操作,然后才解除阻塞。两个函数都以“原子操作”的方式完成“阻塞线程+解锁”或者“重新加锁+解除阻塞”这两个过程。

两个函数都能解除线程的“被阻塞”状态,区别在于:

  • pthread_cond_signal() 函数至少解除一个线程的“被阻塞”状态,如果等待队列中包含多个线程,优先解除哪个线程将由操作系统的线程调度程序决定;
  • pthread_cond_broadcast() 函数可以解除等待队列中所有线程的“被阻塞”状态。

由于互斥锁的存在,解除阻塞后的线程也不一定能立即执行。当互斥锁处于“加锁”状态时,解除阻塞状态的所有线程会组成等待互斥锁资源的队列,等待互斥锁“解锁”。

多线程程序中,如果仅有少数线程会对共享数据进行修改,多数线程只是读取共享数据的值,就适合用读写锁解决“线程间抢夺资源”的问题。

对于无连接的套接字,每个数据包可以选择不同的路径。面向连接的套接字在正式通信之前要先确定一条路径,没有特殊情况的话,以后就固定地使用这条路径来传递数据包了。

静态链接库和动态链接库的作用时机不同,静态链接库会在程序载入内存之前完成所有的链接操作,而动态链接库是在程序载入内存后再进行链接操作。

如何定位死锁?

Linux下排除死锁详细教程(基于C++11、GDB)

GDB之死锁排查

一个简单有效的即时检测线程死锁的方法(附c++源代码)(原创)

C++后端开发(3.2.4)——手写死锁检测组件

效率:std::vector::operator[] > std::vector::at
安全:std::vector::at > std::vector::operator[]
效率:std::vector::emplace_back > std::vector::push_back
效率:std::vector::emplace > std::vector::insert
效率:if(cont.empty()) > if(cont.size() == 0)

安全:std::map::at > std::map::operator[]
效率:std::map::emplace > std::map::insert
向map容器中增添元素,insert()效率更高,更新map容器中的键值对,operator[]效率更高。

效率:std::unordered_map::emplace > std::unordered_map::insert
安全:std::unordered_map::at > std::unordered_map::operator[]

效率:std::unordered_multimap::emplace > std::unordered_multimap::insert

使用shared_ptr 注意事项:

(1).不要用一个原始指针初始化多个shared_ptr,即同一普通指针不能同时为多个 shared_ptr 对象赋值,否则会导致程序发生异常。例如:

int* ptr = new int;
std::shared_ptr<int> p1(ptr);
std::shared_ptr<int> p2(ptr);//错误

(2).不要在函数实参中创建shared_ptr,在调用函数之前先定义以及初始化它。

(3).不要将this指针作为shared_ptr返回出来,否则this指向的局部变量会被析构两次,一次作为栈变量本身被析构,一次被shared_ptr析构。

(4).要避免循环引用。

对于申请的动态数组来说,shared_ptr 指针默认的释放规则是不支持释放数组的,只能自定义对应的释放规则,才能正确地释放申请的堆内存。

weak_ptr可以用来返回this指针。

Q:为什么weak_ptr可以返回this指针?

摘自2023秋招大厂经典面试题及答案整理归纳(221-240)校招必看

std::vector::operator[]使用陷阱:

#include <iostream>
#include <vector>

int main ()
{
	std::vector<int> myvector; 
	
	myvector.reserve(10);
	
	for (unsigned i=0; i<10; i++)
	{
		myvector[i] = i;
	}

	std::cout << "myvector[i]: ";
	for (unsigned i=0; i<10; i++)
	{
		std::cout << myvector[i] << ' ';
	}
	std::cout << std::endl;
	
	std::cout << "auto i: ";
	for (auto i : myvector)
	{
		std::cout << i << ' ';
	}
	std::cout << std::endl;
	
	std::cout << "myvector.size() = " << myvector.size() << std::endl;
	
	return 0;
}

输出:

gavin@gavin-vm:/mnt/hgfs/Shared Folders$ g++ -g test.cpp
gavin@gavin-vm:/mnt/hgfs/Shared Folders$ ./a.out
myvector[i]: 0 1 2 3 4 5 6 7 8 9 
auto i: 
myvector.size() = 0

​正确使用方法:

#include <iostream>
#include <vector>

int main ()
{
	std::vector<int> myvector(10);	// 10 zero-initialized elements
	
	// myvector.reserve(10);
	
	for (unsigned i=0; i<10; i++)
	{
		myvector[i] = i;
	}

	std::cout << "myvector[i]: ";
	for (unsigned i=0; i<10; i++)
	{
		std::cout << myvector[i] << ' ';
	}
	std::cout << std::endl;
	
	std::cout << "auto i: ";
	for (auto i : myvector)
	{
		std::cout << i << ' ';
	}
	std::cout << std::endl;
	
	std::cout << "myvector.size() = " << myvector.size() << std::endl;
	
	return 0;
}

输出:

gavin@gavin-vm:/mnt/hgfs/Shared Folders$ g++ -g test.cpp
gavin@gavin-vm:/mnt/hgfs/Shared Folders$ ./a.out
myvector[i]: 0 1 2 3 4 5 6 7 8 9 
auto i: 0 1 2 3 4 5 6 7 8 9 
myvector.size() = 10

#include <iostream>

using namespace std;

int main()
{
	char t{0};
	
	// [\t]t[\n]
	cin >> t;
	
	if ('\t' == t)
	{
		cout << "\\t == t" << endl;
	}
	else if ('t' == t)
	{
		cout << "\'t\' == t" << endl;
	}
	
	char n{0};
	
	// [\n]n[\n]
	cin >> n;

	if ('\n' == n)
	{
		cout << "\\n == n" << endl;
	}
	else if ('n' == n)
	{
		cout << "\'n\' == n" << endl;
	}
	
	char s{0};

	// [' ']s[\n]
	cin >> s;
	
	if (' ' == s)
	{
		cout << "\' \' == s" << endl;
	}
	else if ('s' == s)
	{
		cout << "\'s\' == s" << endl;
	}
	
	return 0;
}

输入输出: 

[\t]t[\n]    // 输入    
't' == t     // 输出
[\n]         // 输入
n[\n]        // 输入
'n' == n     // 输出
[' ']s[\n]   // 输入
's' == s     // 输出
  • \t \n ' '作为输入的各个值之间的分隔符,它们会被cin读取并丢弃,而不会赋给变量。
#include <iostream>

using namespace std;

int main()
{
	char t{0};
	char n{0};
	char s{0};

	// [\t]t[\n]n[' ']s[\n]
	cin >> t >> n >> s;
	cout << "cin ended." << endl;

	return 0;
}

输入输出:

[\t]t[\n]      // 输入
n[' ']s[\n]    // 输入
cin ended.     // 输出
  • 当输入值(不包括\t \n ' ')的数量达到或超过cin语句要读取变量的数量时,输入\n才会结束本次cin执行。
#include <iostream>

using namespace std;

int main()
{
	char t{0};
	char n{0};
	char s{0};
	int T{0};
	int N{0};
	int S{0};
	
	// tns9 10 32[\n]
	cin >> t >> n >> s;
	cout << t << ' ' << n << ' ' << s << endl;
	cin >> T >> N >> S;
	cout << T << ' ' << N << ' ' << S << endl;
	
	return 0;
}

输入输出:

tns9 10 32[\n]    // 输入
t n s             // 输出
9 10 32           // 输出
  • 当输入值(不包括\t \n ' ')的数量超过cin语句要读取变量的数量,输入\n结束本次cin执行时,本次cin读取输入值的数量就等于它要读取的数量,剩余的输入值保存在键盘缓冲区中,它们将在下次读取输入时被读取。

#include <iostream>

using namespace std;

int main()
{
	// [\t][' '][\n]
	if ('\t' == cin.get())
	{
		cout << "\\t == cin.get()" << endl;
	}
	if (' ' == cin.get())
	{
		cout << "\' \' == cin.get()" << endl;
	}
	if ('\n' == cin.get())
	{
		cout << "\\n == cin.get()" << endl;
	}
	
	return 0;
}

 输入输出:

[\t][' '][\n]       // 输入	 
\t == cin.get()     // 输出 
' ' == cin.get()    // 输出 
\n == cin.get()     // 输出 
  • cin.get() 函数不会跳过' '、\t、\n等特殊字符,所有的字符都能被读入;
  • 输入'\n'时cin.get()返回。
#include <iostream>

using namespace std;

int main()
{
	char c;
	
	// [\t][' '][\n]
	cin.get(c);
	if ('\t' == c)
	{
		cout << "\\t == c" << endl;
	}
	
	cin.get(c);
	if (' ' == c)
	{
		cout << "\' \' == c" << endl;
	}
	
	cin.get(c);
	if ('\n' == c)
	{
		cout << "\\n == c" << endl;
	}
	
	return 0;
}

输入输出:

[\t][' '][\n]    // 输入	 
\t == c          // 输出               
' ' == c         // 输出 
\n == c          // 输出
  •   cin.get(char& c)函数不会跳过' '、\t、\n等特殊字符,所有的字符都能被读入;
  • 输入'\n'时cin.get(char& c)返回。

#include <iostream>

using namespace std;

int main()
{
	char t{0};
	char n{0};
	char s{0};
	
	// tns[\n]
	cin >> t >> n >> s;
	if ('\n' == cin.get())
	{
		cout << "\\n == cin.get()" << endl;
	}
	
	return 0;
}

输入输出: 

tns[\n]            // 输入
\n == cin.get()    // 输出
  • 当输入值(不包括\t \n ' ')的数量等于cin语句要读取变量的数量,输入\n结束本次cin执行时,输入的'\n'不会被读取。

#include <iostream>

using namespace std;

int main()
{
	string str;

	// getline reads the entire line.[\n]
	getline(cin, str);

	cout << str << endl;
	
	return 0;
}

输入输出:

getline reads the entire line.[\n]    // 输入
getline reads the entire line.        // 输出
  •  getline可读取整行,包括前导和嵌入的空格,并将其存储在字符串对象中;
  • 输入\n时getline(cin, str)返回。
#include <iostream>

using namespace std;

int main()
{
	string str;

	// 1234567890[\n]
	getline(cin, str);
	// [\n]
	if ('\n' == cin.get())
	{
		cout << "\\n == cin.get()" << endl;
	}
	cout << "str.size() = " << str.size() << endl;
	
	return 0;
}

输入输出:

1234567890[\n]     // 输入
[\n]               // 输入
\n == cin.get()    // 输出
str.size() = 10    // 输出
  • 如果getline(cin, str)找到了\n,则会提取并丢弃\n(即,\n不会被存储,下一个输入操作将在它之后开始)。
#include <iostream>
 
using namespace std;
 
int main()
{
	string str;
	
	getline(cin, str, ',');
	if ('\n' == cin.get())
	{
		cout << "get \\n" << endl;
	}
	cout << "str.size() = " << str.size() << endl;
	
	return 0;
}

输入输出:

1234567890[\n]     // 输入
,[\n]              // 输入
\n == cin.get()    // 输出
str.size() = 11    // 输出
  • 如果getline(cin, str, ',')在找到','之前找到了\n,则会提取\n并将其存储到str中,getline不会因输入\n而返回;
  • 如果getline(cin, str, ',')在找到','之后输入了\n,则会提取并丢弃','(即,','不会被存储,下一个输入操作将在','之后开始),getline不会因输入','而返回,而会因输入','之后输入的\n而返回,','之后的\n不会被getline提取。

istream 类中从输入流(包括文件)中读取数据的成员函数(>>、get(char& c)、std::istream::getline)以及std::getline(string),在把输入数据都读取完后再进行读取,就会返回 EOF。

#include <iostream>

using namespace std;

int main()
{
	int i{ 0 };
	
	while (cin >> i)
	{
		cout << i << endl;
	}

	if (EOF == cin.get())
	{
		cout << "EOF" << endl;
	}

	cin >> i;
	cout << i << endl;

	if (true == cin.eof())
	{
		cin.clear();
		cout << "cin.clear()" << endl;
	}
	
 	cin >> i;
	cout << i << endl;
	
	return 0;
}

输入输出:

123[\n]             // 输入   
123                 // 输出
456[\n]             // 输入
456                 // 输出
EOF                 // 输入Ctrl+D后的输出,EOF未被cin读取,而被cin.get()读取。
456                 // 虽然cin.get()读取了EOF,但是cin仍处于eof状态,于是第19行并没有从输入流读入i,i维持了原来的值456。
cin.clear()         // 第24行清除cin内部的错误标记,此后cin又能正常读入了。
789                 // 因此,789在第28行被读入i。
789                 // 输出
  • cin >>、cin.get(char& c)、cin.getline、getline(cin, str)的返回值是函数所作用的对象cin的引用(istream&类型);
  • cin对象可以被自动转换成bool类型;
  • Ctrl+D 代表输入结束EOF;
  • cin 在正常读取时返回 true,遇到结束标志EOF时返回 false;
  • EOF不会被cin读取。
#include <iostream>

using namespace std;

int main()
{
	int i{ 0 };
	
	while (cin >> i)
	{
		cout << i << endl;
	}

	if (EOF == cin.get())
	{
		cout << "EOF" << endl;
	}
	if (EOF == cin.get())
	{
		cout << "EOF" << endl;
	}
	
	return 0;
}

输入输出:

123123              // 输入123[Ctrl+D][Ctrl+D]后输出123
EOF                 // 输出
EOF                 // 输出

无法解释。

#include <iostream>

using namespace std;

int main()
{
	int i{ 0 };
	
	while (cin >> i)
	{
		cout << i << endl;
	}
	
	cout << "i = " << i << endl;
	
	if ('a' == cin.get())
	{
		cout << 'a' << endl;
	}
	if ('\n' == cin.get())
	{
		cout << "\\n" << endl;
	}

	if (true == cin.fail())
	{
		cin.clear();
		cout << "cin.clear()" << endl;
	}

	if ('a' == cin.get())
	{
		cout << 'a' << endl;
	}
	if ('\n' == cin.get())
	{
		cout << "\\n" << endl;
	}
	
	return 0;
}

输入输出:

123[\n]        // 输入
123            // 输出
a[\n]          // 输入,出错,要读入数字,却读入了字符'a'。
i = 0          // 输出,可能由于读入出错,i被置为0。
cin.clear()    // 因cin出错,于是第16、20行并没有从输入流读入字符。第27行清除cin内部的错误标记,此后cin又能正常读入了。
a              // 输出,因此,a在第31行被读取。                    
\n             // 输出,\n在第35行被读取。
  • 当输入值类型与读取目的变量类型不一致时,cin返回false。

以下情况将导致迭代器失效:

一旦 vector 容器的内存被重新分配(向容器添加元素、增加容器容量),这可能会导致之前创建的和 vector 容器中元素相关的所有指针、引用以及迭代器失效。

解决办法:重新赋值一下之前的迭代器。

list 容器在进行插入(insert())、接合(splice())等操作时,都不会造成原有的 list 迭代器失效,甚至进行删除操作,而只有指向被删除元素的迭代器失效,其他迭代器不受任何影响。

迭代器失效的几种情况总结

以下红色文字来自《C语言中文网》,里面的内容有待提取出来,合并到上面的“以下情况将导致迭代器失效”中。

深度剖析C++无序容器的底层实现机制

默认情况下,无序容器的最大负载因子为 1.0。如果操作无序容器过程中,使得最大复杂因子超过了默认值,则容器会自动增加桶数,并重新进行哈希,以此来减小负载因子的值。需要注意的是,此过程会导致容器迭代器失效,但指向单个键值对的引用或者指针仍然有效。

C++ unordered_map迭代器的用法

需要注意的是,在操作 unordered_map 容器过程(尤其是向容器中添加新键值对)中,一旦当前容器的负载因子超过最大负载因子(默认值为 1.0),该容器就会适当增加桶的数量(通常是翻一倍),并自动执行 rehash() 成员方法,重新调整各个键值对的存储位置(此过程又称“重哈希”),此过程很可能导致之前创建的迭代器失效。

所谓迭代器失效,针对的是那些用于表示容器内某个范围的迭代器,由于重哈希会重新调整每个键值对的存储位置,所以容器重哈希之后,之前表示特定范围的迭代器很可能无法再正确表示该范围。但是,重哈希并不会影响那些指向单个键值对元素的迭代器。

stringstream

#include <iostream>
#include <sstream>

using namespace std;
 
int main()
{
	std::stringstream stream;
	string str;
 
	while(1)
	{
        // 1 在多次数据类型转换中使用同一个stringstream对象,记住在每次转换前必须要调用stringstream的成员函数
        //   clear(),否则不能得到数据类型转换的正确结果。
		// 2 clear()不会清空流的内容,它只是重置了流的状态标志。
        // 3 在多次转换中重复使用同一个stringstream对象(而不是每次都创建一个新的对象)最大的好处在于效率。
        //   stringstream对象的构造和析构函数通常是非常耗费CPU时间的。
		stream.clear();

        // 如果在程序中用同一个流反复读写大量数据,将会造成大量的内存消耗,这时需要适时地用stream.str("")清空
        // 缓冲。去掉下面这行注释,清空stringstream的缓冲,每次循环内存消耗将不再增加。
		//stream.str("");

        // 不要企图用stream.str().resize(0)或stream.str().clear()来清除缓冲,使用它们似乎可以让
        // stringstream的内存消耗不要增长得那么快,但仍然不能达到清除stringstream缓冲的效果。
		//stream.str().resize(0);
		//stream.str().clear();

		stream << "you see see you";
		stream >> str;

		// 去掉下面这行注释,看看每次循环,内存消耗会增加多少。
		cout << "Size of stream = " << stream.str().length() << endl;
	}
 
	return 0;
}

摘自string和stringstream用法

C++ cout格式化输出

#include <bits/stdc++.h>

using namespace std;

int main()
{
	double pi{ 123.1415926 };
	int a{ 43981 };
	
	// 成员与算子所起的作用都不是一次性的,流操纵算子较成员函数简便。
	
	
	cout << (43981 == a) << endl;	// 1
	
	// cout成员方法格式化输出布尔值
	cout.setf(ios::boolalpha);
	cout << (43981 == a) << endl;	// true
	cout.unsetf(ios::boolalpha);

	/************************推荐**********************/
	//   流操纵算子格式化输出布尔值
	cout << boolalpha << (43981 == a) << endl;	// true
	cout << resetiosflags(ios::boolalpha);
	
	
	// cout成员方法格式化输出十六进制
	// 默认设置了基标志ios::dec,又要设置与之矛盾的基标志ios::hex时,就应该清除原先的基标志ios::dec。
	cout.unsetf(ios::dec);
	cout.setf(ios::hex);
	cout << a << endl;	// abcd
	cout.setf(ios::showbase);
	cout << a << endl;	// 0xabcd
	cout.setf(ios::uppercase);
	cout << a << endl;	// 0XABCD
	cout.unsetf(ios::hex | ios::showbase | ios::uppercase);

	//   流操纵算子格式化输出十六进制
	// 默认设置了基标志ios::dec,又要设置与之矛盾的基标志ios::hex时,就应该清除原先的基标志ios::dec。
	cout << resetiosflags(ios::dec) << setiosflags(ios::hex) << a << endl;	// abcd
	cout << showbase << a << endl;	// 0xabcd
	cout << uppercase << a << endl;	// 0XABCD
	cout << resetiosflags(ios::hex | ios::showbase | ios::uppercase);

	/************************推荐**********************/
	//   流操纵算子格式化输出十六进制
	cout << hex << a << endl;	// abcd,等效于cout << setbase(16) << a << endl;
	cout << showbase << a << endl;	// 0xabcd
	cout << uppercase << a << endl;	// 0XABCD
	cout << resetiosflags(ios::hex | ios::showbase | ios::uppercase);
	
	
	cout << pi << endl;		// 123.142,6位有效数字
	
	// cout成员方法格式化输出浮点数
	int defaultPrecision = cout.precision(5);
	cout << pi << endl;		// 123.14,在使用非fixed方式输出的情况下,precision   为有效数字最多的位数
	cout.setf(ios::fixed);
	cout << pi << endl;		// 123.14159,fixed默认精确到小数点后第6位,在使用fixed方式输出的情况下,precision是小数点后面应保留的位数
	cout.unsetf(ios::fixed);
	cout.precision(defaultPrecision);

	/************************推荐**********************/
	//   流操纵算子格式化输出浮点数
	cout << setprecision(5) << pi << endl; // 123.14,在使用非fixed方式输出的情况下,setprecision为有效数字最多的位数
	cout << fixed << pi << endl; // 123.14159,fixed默认精确到小数点后第6位,在使用fixed方式输出的情况下,setprecision是小数点后面应保留的位数
	cout << resetiosflags(ios::fixed);
	
	return 0;
}

std::function详解

【C++】C++11的std::function和std::bind用法详解

C11:std::unique_lock和std::lock_guard的区别

std::unique_lock与std::lock_guard区别

C++多线程——读写锁shared_lock/shared_mutex​​​​​​​

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值