C++学习整理

C++常用代码示例

一、c_str()函数

// 标准库的string类提供了三个成员函数来从一个string得到c类型的字符数组
// c_str():生成一个const char *指针,指向以空字符终止的数组
// 这个数组应该是string类内部的数组

#include<iostream>
#include<cstring>
using namespace std;
int main()
{   
    // string-->char *
    // c_str()函数返回一个指向正规C字符串的指针,内容与本string串相同
    // 这个数组的数据是临时的,当有一个改变这些数据的成员函数被调用后,其中的数据就会失效。
    // 因此,要么先转换;要么把它的数据复制到用户可以自己管理的内存中
    const char *c;
    string s = "1234";
    c = s.c_str();
    cout << s << endl;
    cout << c << endl;
    s = "abcde";
    cout << c << endl;
}
// 标准库的string类提供了三个成员函数来从一个string得到c类型的字符数组
// c_str():生成一个const char *指针,指向以空字符终止的数组
// 这个数组应该是string类内部的数组

#include<iostream>
#include<cstring>
using namespace std;
int main()
{   
    // 更好的方法是将string数组中的内容复制出来,所以会用到strcpy()这个函数
    char *c = new char[20];
    string s = "1234";
    // c_str()返回一个客户程序可读不可改的指向字符数组的指针,不需要手动释放或删除这个指针
    strcpy(c, s.c_str());
    cout << s << endl;
    cout << c << endl;
    s = "abcde";
    cout << c << endl;
}

二、字符串初始化表达

#include<iostream>
int main(){
	std::string cmdLine("get_operation_mode");
	std::cout << cmdLine << std::endl;
}

三、pair的基本使用

pair是将2个数据组合成一组的数据,当需要这样的需求时就可以使用pair。

如STL中的map就是将key与value放在一起来保存。

另一个应用是:当一个函数需要返回2个数据的时候,可以选择pair

pair的实现是一个结构体,主要的两个成员变量是 first second

因为是使用struct不是class,所以可以直接使用pair的成员变量。

pair类型定义在 #include <utility> 头文件中,定义如下:

类模板:template<class T1, class T2> struct pair

参数:T1是第一个值的数据类型, T2是第二个值的数据类型

功能:pair将一对值(T1和T2)组合成一个值

​ 这一对值可以具有不同的数据类型

​ 两个值可以分别用pair的两个公有函数first和second访问

定义(构造函数):

//创建一个空的pair对象(使用默认构造),它的两个元素分别是T1和T2类型,采用值初始化。
pair<T1, T2> p1;            

//创建一个pair对象,它的两个元素分别是T1和T2类型,其中first成员初始化为v1,second成员初始化为v2。
pair<T1, T2> p1(v1, v2);    

// 以v1和v2的值创建一个新的pair对象,其元素类型分别是v1和v2的类型。
make_pair(v1, v2);          

// 两个pair对象间的小于运算,其定义遵循字典次序:如 p1.first < p2.first 或者 !(p2.first < p1.first) && (p1.second < p2.second) 则返回true。
p1 < p2;                    

// 如果两个对象的first和second依次相等,则这两个对象相等;该运算使用元素的==操作符。
p1 == p2;                  
    
// 返回对象p1中名为first的公有数据成员
p1.first;                  

// 返回对象p1中名为second的公有数据成员
p1.second;                 

pair的创建和初始化:

pair包含两个数值,与容器一样,pair也是一种模板类型。

在创建pair对象时,必须提供两个类型名,两个对应的类型名的类型不必相同。

pair<string, string> anon;        // 创建一个空对象anon,两个元素类型都是string
pair<string, int> word_count;     // 创建一个空对象 word_count, 两个元素类型分别是string和int类型
pair<string, vector<int> > line;  // 创建一个空对象line,两个元素类型分别是string和vector类型

当然也可以在定义时进行成员初始化:

// 创建一个author对象,两个元素类型分别为string类型,并默认初始值为James和Joy。
pair<string, string> author("James","Joy");    

pair<string, int> name_age("Tom", 18);

// 拷贝构造初始化
pair<string, int> name_age2(name_age);    

pair类型的使用相当的繁琐,如果定义多个相同的pair类型对象,可以使用typedef简化声明:

typedef pair<string, string> Author;
Author proust("March", "Proust");
Author Joy("James", "Joy");

变量间赋值:

pair<int, double> p1(1, 1.2);
pair<int, double> p2 = p1;     // copy construction to initialize object
pair<int, double> p3;
p3 = p1;

pair对象的操作:

访问两个元素操作可以通过first和second访问:

pair<int, double> p1;

p1.first = 1;

p1.second = 2.5;

cout<<p1.first<<" "<<p1.second<<endl;

//输出结果:1 2.5


string firstBook;
if(author.first=="James" && author.second=="Joy")
    firstBook="Stephen Hero";

生成新的pair对象:

还可以利用make_pair创建新的pair对象:

pair<int, double> p1;
p1 = make_pair(1, 1.2);
cout << p1.first << "\t" << p1.second << endl;
// output: 1  1.2

int a = 8;
string m = "James";
pair<int, string> newone = make_pair(a, m);
cout << newone.first << "\t" << newone.second << endl;
// output: 8  James

通过tie获取pair元素值:

在某些情况下函数会以pair对象作为返回值时,可以直接通过std::tie进行接收,即用于解包std::pair

比如:

#include<iostream>
#include<tuple>
#include<string>

// 标准库的命名空间都是std

std::pair<std::string, int> getPreson() {
    return std::make_pair("Seven", 25);
}

int main(int argc, char **argv) {
    std::string name;
    int ages;

    std::tie(name, ages) = getPreson();

    std::cout << "name: " << name << ", ages: " << ages << std::endl;

    return 0;
}

// 输出: name: Seven, ages: 25

四、vector的基本用法

向量类型,允许容纳同一种数据类型的许多元素,因此称之为容器。

相当于一个动态数组。当程序员无法知道自己需要的数组大小时,选择vector

vector是C++ 的STL的一个重要成员,使用它时需要包含头文件:#include<vector>

构造函数语法:

vector();
vector(size_type num, const TYPE &val);
vector(const vector &from);
vector(input_iterator start, input_iterator end);

变量声明:

//无参数 - 构造一个空的vector,
vector<int> a;           

//定义了10个整型元素的向量(尖括号中为元素类型名,它可以是任何合法的数据类型),但没有给出初值,其值是不确定的
vector<int> a(10);       

//定义了10个整型元素的向量, 且给出每个元素的初值为1
vector<int> a(10, 1);     

//用b向量来创建a向量,整体赋值, 拷贝构造
vector<int> a(b);        

//移动构造
vector<int> v3 = a;      

//定义了a值为b中第0个到第2个(共3个)元素
vector<int> a(b.begin(), b.begin + 3);   

//从数组中获得初值,b[0]~b[5]
int b[7] = {1, 2, 3, 4, 5, 9, 8};
vector<int> a(b, b + 6);   

vector对象的重要操作,举例说明如下:

(1)a.assign(b.begin(), b.begin() + 3);              // b为向量,将b的0~2个元素构成的向量赋给a
(2)a.assign(4, 2);                                  // 是a只含4个元素,且每个元素为2
(3)a.back();                                        // 返回a的最后一个元素
(4)a.front();                                       // 返回a的第一个元素
(5)a[i];                                            // 返回a的第i个元素,当且仅当a[i]存在2013-12-07
(6)a.clear();                                       // 清空a中的元素
(7)a.empty();                                       // 判断a是否为空,空则返回ture,不空则返回false
(8)a.pop_back();                                    // 删除a向量的最后一个元素
(9)a.erase(a.begin() + 1, a.begin() + 3);           // 删除a中第1个(从第0个算起)到第2个元素,也就是说删除的元素从a.begin()+1算起(包括它)一直到a.begin()+3(不包括它)

(10)a.push_back(5);                                 // 在a的最后一个向量后插入一个元素,其值为5
(11)a.insert(a.begin() + 1, 5);                     // 在a的第1个元素(从第0个算起)的位置插入数值5,如a为1,2,3,4,插入元素后为1,5,2,3,4

(12)a.insert(a.begin() + 1, 3, 5);                  // 在a的第1个元素(从第0个算起)的位置插入3个数,其值都为5
(13)a.insert(a.begin() + 1, b + 3, b + 6);          // b为数组,在a的第1个元素(从第0个算起)的位置插入b的第3个元素到第5个元素(不包括b+6),如b为1,2,3,4,5,9,8,插入元素后为1,4,5,9,2,3,4,5,9,8

(14)a.size();                                       // 返回a中元素的个数;
(15)a.capacity();                                   // 返回a在内存中总共可以容纳的元素个数
(16)a.resize(10);                                   // 将a的现有元素个数调至10个,多则删,少则补,其值随机
(17)a.resize(10, 2);                                // 将a的现有元素个数调至10个,多则删,少则补,其值为2
(18)a.reserve(100);                                 // 将a的容量(capacity)扩充至100,也就是说现在测试a.capacity()的时候返回值是100,
                                                      // 这种操作只有在需要给a添加大量数据的时候才显得有意义,因为这将避免内存多次容量扩充操作(当a的容量不足时电  脑会自动扩容,当然这必然降低性能) 

(19)a.swap(b);                                      // b为向量,将a中的元素和b中的元素进行整体性交换
(20)a.begin();                                      // 返回指向容器第一个元素的迭代器
(21)a.end();                                        // 返回指向容器最后一个元素的迭代器
(22)a==b;                                           // b为向量,向量的比较操作还有!=,>=,<=,>,<
(23)sort(a.begin(), a.end());                       // 对a中的从a.begin()(包括它)到a.end()(不包括它)的元素进行从小到大排列

(24)reverse(a.begin(),a.end());                     // 对a中的从a.begin()(包括它)到a.end()(不包括它)的元素倒置,但不排列,如a中元素为1,3,2,4,倒置后为4,2,3,1

(25)copy(a.begin(),a.end(),b.begin() + 1);          // 把a中的从a.begin()(包括它)到a.end()(不包括它)的元素复制到b中,从b.begin()+1的位置(包括它)开始复制,覆盖掉原有元素

(26)find(a.begin(), a.end(), 10);                   // 在a中的从a.begin()(包括它)到a.end()(不包括它)的元素中查找10,若存在返回其在向量中的位置

注意:下面向vector中添加元素的方式是错误的(这种方式是C语言中数组添加元素的方式):

vector<int> vec_int_objs;
for(int i = 0; i < 10; ++i){
    vec_int_objs[i] = i;  // 这样添加元素是错误的,务必注意
}

C++11 新增关于vector的特性:

a.cbegin();        // 返回指向容器中第一个元素的const_iterator
a.cend();          // 返回指向容器中最后元素的下一个位置(即尾后元素)const_iterator
a.crbegin();       // 反转迭代器, 返回指向容器中尾后元素的const_iterator
a.crend();         // 反转迭代器, 返回指向容器中第一个元素的const_iterator
a.emplace();       // 相对于insert功能,但比它更有效率
a.emplace_back();  // 相对于push_back, 但比它更有效率

emplace_back能通过参数构造对象,不需要拷贝或者移动内存。相比push_back能更好地避免内存的拷贝与移动,使容器插入元素的性能得到进一步提升。

由此,在大多数情况下应该优先使用emplace_back来代替push_back。

例子:

#include <iostream>
#include <vector>

int main ()
{
  std::vector<int> myvector = {10, 20, 42, 33, 50};
  std::cout << "myvector contains:";

  // test cbegin, cend
  for (auto it = myvector.cbegin(); it != myvector.cend(); ++it){
      std::cout << ' ' << *it;
  }
  std::cout << '\n';
  
  std::cout << "myvector contains:";
  // test crbegin, crend
  for (auto it = myvector.crbegin(); it != myvector.crend(); ++it){
      std::cout << ' ' << *it;  
  }
  std::cout << '\n';
  return 0;
}
// 输出结果:
// myvector contains: 10 20 42 33 50
// myvector contains: 50 33 42 20 10

emplace和emplace_back例子:

#include <iostream>
#include <vector>

// reference: http://www.cplusplus.com/reference/vector/vector/emplace_back/
int test_emplace_1()
{
	/*
		template <class... Args>
		void emplace_back (Args&&... args);
	*/
	std::vector<int> myvector = {10, 20, 30};
 
	myvector.emplace_back(100);
	myvector.emplace_back(200);
 
	std::cout << "myvector contains:";
	for (auto &x : myvector){
        std::cout << ' ' << x;
    }
	std::cout << '\n';
	// output 10 20 30 100 200
}

int test_emplace_2()
{
		
	/*
		template <class... Args>
		iterator emplace (const_iterator position, Args&&... args);
	*/
	std::vector<int> myvector = {10, 20, 30};
     
    // return an iterator that points to the newly emplaced element. 
	auto it = myvector.emplace(myvector.begin(), 100);
	myvector.emplace(it, 200);
	myvector.emplace(myvector.end(), 300);
 
	std::cout << "myvector contains:";
	for (auto &x : myvector){
        std::cout << ' ' << x;
    }
	std::cout << '\n'; 
    // output: 10 200 100 20 30 300
}

int main(void)
{
	test_emplace_1();
	test_emplace_2();
	
	return 0;
}

五、C++泛型编程

泛型编程

​ 所谓的泛型就是指忽略参数类型限制,独立于任何特定数据类型进行编码。

使用泛型程序时,需要提供具体程序实例所操作的数据类型或者值。我们经常用到的STL容器、迭代器、及算法都是泛型编程的例子。

  • 模板是C++支持参数化多态的工具。使用模板可以使用户(即程序员)为类或者函数声明一种一般模式,使得类中的某些数据成员或者成员函数的参数、返回值为任意类型
  • 模板是一种对类型进行参数化的工具(把它看成一种数据格式转换的工具即可)
  • 通常有两种形式:1、函数模板 2、类模板
  • 函数模板针对只有参数类型不同的函数
  • 类模板针对仅数据成员成员函数类型不同的类
  • 使用模板的目的是能够让程序员编写与类型无关的代码。

模板函数

普通定义形式

template<typename T>
int func(const T &a1, const T &a2)
{
    ...
}
template <class T>
inline int func(const T &a1, const T&a2)
{
    ...
}
tempalte<typename T1, typename T2, typename T3>
T1 func(const T2 &t2, const T3 &t3)
{
    ...
}
//调用方法
func<long>(i, log);

如果类型的定义顺序与调用顺序不一样的话,则需要在声明的时候指定类型的顺序:

tempalte<typename T1, typename T2, typename T3>
T3 func(const T1 &t1, T2 &t2)
{
    ...
}
//调用方法
func<long,  int , long>(12, 34);

模板类

//定义方式
template <class Type> 
class Queue
{
    ...
    ...
}
//使用方法
Queue<int> qi;
  • 模板参数表不能为空
  • 由编译器根据实参实例化模板,模板在实例化的时候会对参数类型进行语法检查
  • 作用域由声明之后直到模板声明或定义的末尾处使用
  • 模板形参的名字只能在同一模板形参表中使用一次
  • 声明和定义的模板形参名称可能不一样
  • 模板类型形参可以作为类型说明符用在模板中任何地方,与内置类型说明符类类型说明符的使用方式完全相同
  • 对模板的非类型形参而言,求值结果相同的表达式将认为是等价的,调用相同的实例
  • 编写模板代码时,对实参类型的要求要尽可能少

非类型模板形参

template <class T, size_t N> 
void func(T (&parm)[N])
{
    //此时,N将直接用值代替
}

关于模板编译

​ 发现错误一般分为三个阶段:

  • 编译模板定义本身,可以检测模板本身的语法问题,例如漏掉分号,拼写错误等
  • 编译器见到模板的使用时,检测参数的个数、类型是否合法
  • 模板实例化期间,检测类型相关的错误

模板实例化

​ 类模板在引用实例类模板类型时实例化,函数模板在调用它或者用它对函数指针进程初始化或者赋值时实例化,在使用函数模板时,编译器通常会为我们推断模板实参。

​ 当编译器看到模板定义时,不立即产生代码,只有在看到模板调用时,编译器才会产生对应的实例,类型相关的错误才会被检查出来。

模板编译模型

​ 通常情况下,实例化一个对象或者调用一个函数时,编译器不需要看到函数或者类的定义,只有在链接的时候才会去关心类或者函数的定义。但是模板不一样,编译器在实例化模板时,必须看到模板的定义才会编译通过。

包含编译模型

//header file
#ifndef xx_H_
#define xx_H_
template <typename T>
int func(T1 &t1, T2 &t2);
#include "oo.cpp" //模板定义文件
#endif
//oo.cpp
template<typanem T>
int func(T1 &t1, T2 &t2)
{
    ...
} 

类模板成员函数

  • 必须以关键字template开头,后接类的模板形参
  • 必须指出它是哪个类的成员
  • 类名必须包含其模板形参
  • 类模板的成员函数本身也是模板函数,像任何其他函数模板一样,需要使用类模板的成员函数产生该成员的实例化,也就是说只有在使用的时候才会被实例化
  • 类模板的形参定义在实例化类的时候确定了,所以调用的时候用于进行参数的常规类型转换
template <class T> ret-type Queue<T>::member_func_name
{
    //define 
}

非类型形参的模板实参

  • 非类型模板实参必须是编译时常量表达式
template <int hi, int wid>
class Screen
{
	public:
    	Screen():{}
	private:
		std::string screen;
		std::string::size_type cursor;
		std::string::size_type height, width;
}

// 实例化方法, 参数必须是编译时常量表达式
Screen<24, 80> hp2621;

类模板中友元声明

  • 普通友元,将由原关系授予制定的类或函数

    template <class Type> 
    class Bar
    {
        friend class FooBar;
        ...
    }
    

    FooBar的成员可以访问Bar类任意实例的private &protected成员

  • 一般模板友元关系

    template <class Type>
    class Bar
    {
        template <class T> friend class Fool;
        template <class T> friend void templ_fcnt(const T&);
        ...
    }
    

    表示Fool和templ_fcnt的任意实例都可以访问Bar的任意实例的private和protected成员。

  • 特定模板友元关系

    模板类只授权对特定友元实例的访问权

    template <class T> class Foo2;
    template <class void templ_fcnt(const T&);
    template <class Type>
    class Bar 
    {
        friend class Foo2<char*>;
        friend void templ_fcnt<char*>(char *const &);
    }
    
  • 更通用的形式

    template <class T> class Foo2;
    template <class T> void templ_fcnt(const T&);
    template <class Type>
    class Bar 
    {
        friend class Foo2<Type>;
        friend void templ_fcnt<Type>(const Type &);
        ...
    
    }
    

    这样每个类型的类模板实例与对应的类型友元建立了——映射关系

  • 声明依赖性

    如果模板类授权给所有友元实例访问private和protected成员时,编译器将友元声明当做类或者函数的声明对待;但是如果指定到特定类型时,必须在前面声明类或者函数。参考上面特定模板友元关系和一般友元关系的声明。

    同时,如果没有提前告诉编译器该友元是一个模板,编译器则认为友元是一个普通非模板函数或者非模板类。

  • 模板类的成员模板

    这个名字确实有点绕,其本质意思就是模板类的成员函数也希望有自己的参数类型,看如下例子:

    template <class Type>
    class Queue
    {
    public:
        template <class It>
        Queue(It begin, It end):
            head(0), tail(0)
            {
                copy_elems(beg, end);
            }
    
        template <class Iter>
        void assign(Iter , Iter);
    private:
        template <class Iter>
        void copy_elems(Iter, Iter);
    }
    

    在类模板的外部定义模板成员,必须包含类模板的形参和模板成员的模板形参:

    template <class Type>  // 类模板的形参
    template <class Iter>  // 成员模板形参
    void Queue<Type>::assign(Iter begin, Iter end)
    {
        ...
    }
    

    与其他成员一样,成员模板也只有在被使用时才会实例化。

  • 类模板的static成员

    template <class Type>
    class Bar
    {
    public:
        static std::size_t count(){
      return ctr};
    
    private:
        static std::size_t ctr;
    }
    

    实例化原则是:相同类型的实例共享一个static成员,例如Bar类型的实例共享一个static成员ctr,Bar类型的实例共享一个static成员ctr;

    使用方法:

    Bar<int> bar1, bar2;
    size_t ct = Bar<int>::count();
    
  • 模板特化

    由于模板的定义中,其操作都是依赖实例化的类型是否支持该操作或者操作的结果与预期是否相匹配,例如:

    template <class Type>
    int compare(const Type& t1, const Type &t2)
    {
        if(t1 > t2) return 1;
    
        if(t1 < t2> return -1;
    
        return 0;
    }
    

    在上面的例子中,如果用char *去实例化模板时,函数将比较两个指针,很明显与预期的结果不吻合。此时可以通过模板特例来解决。

  • 函数模板特化

    函数模板特例化形式如下:

    1. 在关键字template后面接一对<>
    2. 在<>中放置模板形参
    3. 函数形参表
    4. 函数体

    例如:

    template <>
    int compare<const char*>(const char *t1, const char *t2)
    {
        return strcmp(t1, t2);
    } 
    

    如果有多个模板形参,则依次排列即可。

  • 类模板特化

    • 定义类特化
    template <>
    class Queue<const char*>
    {
        ...
    }
    

    需要在类特化的外部定义普通成员函数时,成员之前不能加template<>标记:

    void Queue<const char*>::push(const char* val)
    {
        ...
    }
    
  • 特化成员而不特化类

    template <>
    void Queue<const char *>::push(const char* const &val)
    {
        ...
    }
    
    template<>
    void Queue<const char*>::pop()
    {
        ...
    } 
    

    现在,类 类型Queue:

    template <class T1, class T2>
    class tem
    {
        ...
    };
    
    //partial specialization :fixes T2 as int and allows T2 to vary.
    template <class T1>
    class tem<T1, int>
    {
        ...
    }
    

    使用方法:

    tem<int, string> foo;  // 调用普通的类模板
    tem<string, int> bar;  // 调用偏例化版本 
    
  • 重载与函数模板

    函数模板可以重载:可以定义有相同名字但形参数据或类型不同的多个函数模板,也可以定义与函数模板有相同名字的普通非模板函数。

    不过从实践来看,设计既包含函数模板又包含非模板函数的重载函数集合是困难的,因为你会使函数的用户感到奇怪,定义函数模板特化几乎总是比使用非模板版本更好。

六、为什么 i++ 不能做左值,而++i可以?

示例代码:

#include<iostream> 
int main(){
     int i = 0;
     
     // 错误:error: lvalue required as unary ‘&’ operand(左值要求是一元' & '操作数)
     int *ip = &(i++);  // 错误
    
     int *qp = &(++i);  // 正确
     
     std::cout << *ip << " " << std::endl;
     std::cout << *qp << std::endl;
}

左值:

​ C/C++语言中可以放在赋值符号=左边的变量,即具有对应的可以由用户(即编码人员)访问的存储单元,并且能够由用户去改变其值的变量(或者称之为对象)。

​ 左值表示存储在计算机内存的对象,而不是常量或者计算的结果。

​ 左值是代表一个内存地址的值,通过这个内存地址,就可以对内存进行读并且写(主要是能写)操作;这就是左值可以被赋值的原因。

​ 有名字,可以取地址的值——声明的变量值,比如 int a

右值:

​ 当一个符号或者常量放在操作符(即 + - * / 或者其他任何进行操作的符号)右边的时候,计算机就读取它们的“右值”,也就是其代表的真实值。

​ 无名字,不可以取地址的值——临时变量值,字面常量值,比如:加法表达式,函数返回值,字符常量等

简单来说就是:左值相当于内存地址值,右值相当于数据值。

看完左值的定义就不难理解为什么取地址运算符需要作用在一个左值对象上了。为何i++++i有如此的区别呢?

原因是:i++不是存储在内存中的值,它们的具体函数实现如下:

// 前缀形式
int &::operator++()  // 这里返回的是一个引用形式,就是说函数返回值也可以作为一个左值使用
{
    // 函数本身无参,意味着是在自身空间内增加1
    *this += 1;    // 增加
    return *this;  // 取回值
}    
// 后缀形式
const int int::operator++(int){
    // 函数返回值是一个非左值类型的,与前缀形式的差别所在
    // 函数带参,说明有另外的空间开辟
    int oldValue = this;  // 取回值
    ++this;               // 增加
    return oldValue;      // 返回被取回的值
}

七、GDB调试技巧:打印vector中的元素

​ 我们平常在使用GDB调试程序的时候,往往需要查看一个STL容器里面存储的元素的是啥。但是使用GDB的p命令打印容器,我们会得到一堆乱七八糟的东西。

​ 怎么让GDB打印出容器实际存储的元素值呢?

​ GDB提供了一个call指令,可以在调试过程中调用任意一个函数,并且可以给函数传入不同的参数。例如:

#include<iostream>
using namespace std;

int add(int a, int b){ 
    return a + b;
};

int main()
{
    cout << add(1, 2) << endl;
    return 0;
}

启动GDB开始调试程序,我们就可以使用call指令来调用add()函数了:

bzl@bzl ~/cppcode/Temp o gdb a.out -q
Reading symbols from a.out...done.
(gdb) l
1	#include<iostream>
2	using namespace std;
3	int add(int a, int b){
4	    return a + b;
5	};
6	int main()
7	{
8	    cout << add(1, 2);
9	    return 0;
10	}
(gdb) start
Temporary breakpoint 1 at 0x40075e: file example.cpp, line 8.
Starting program: /home/bzl/cppcode/Temp/a.out 

Temporary breakpoint 1, main () at example.cpp:8
8	    cout << add(1, 2);
(gdb) call add(3, 5)
$1 = 8
(gdb) call add(3, 15)
$2 = 18
(gdb)

可以看到,我们在调试的过程中,可以随意地调用函数。那么有了call指令,打印容器的元素值就很简单了。我们写一个打印函数,在需要查看的时候调用一下不就行了?以vector为例,示例如下:

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

// 这种风格:先声明,后定义。有点意思
void pv(vector<int>& nums);
void pv(vector<int>& nums, size_t index);


int main()
{
    vector<int> nums(1,3);
    for (size_t i = 0; i < 20; i++) {
        nums.push_back(i);
    }
    return 0;
}

void pv(vector<int>& nums)
{
    cout << "std::vector of length " << nums.size() << ", capacity " << nums.capacity() << " = {";
    for (size_t i = 0; i < nums.size(); i++) {
        cout << nums[i];
        if (i + 1 < nums.size()) {
            cout << ",";
        }
    }
    cout << "}" << endl;
}

void pv(vector<int>& nums, size_t index) 
{
    if (index >= nums.size()) {
        cout << "index should be in [0, " << nums.size() << ")" << endl;
        return;
    }
    cout << nums[index] << endl;
}

可以看到,我们使用call调用pv函数,可以打印出我们想要的结果。其他的容器,如list或者map等,我们只需要定制一个打印函数,在调试的时候使用call调用即可。

八、map基本用法

map中的元素是一些关键字-值(key-value)对:关键字起到索引的作用,值则表示与索引相关联的数据。

定义在头文件map中,即使用时需要 #include<map>

定义map:

map<string, size_t> word_count_map; // 空容器

map<string, string> authors = {

​ {“Joyce”, “James”},

​ {“Austen”, “Jane”},

​ {“Dickens”, “Charles”}

};

注意:当初始化一个map时,必须提供关键字类型和值类型。我们将每个关键字-值包围在花括号中:{key, value} 来

指出它们一起构成了map中的一个元素。在每个花括号中,关键字是第一个元素,值是第二个。

关键字是const这一特性意味着不能将关联容器传递给修改或重排容器元素的算法,因为这类算法需要向元素写入值。

map中的元素是pair,其第一个元素是const的。

示例代码:

// 统计每个单词在输入中出现的次数
#include<map>
#include<string>
#include<iostream>
using namespace std;

int main(){
  // 注意: map string等数据类型是在std标准命名空间中的,不要漏写了
  map<string, size_t> word2count_map;  // string到size_t的空map
  string word;
  while (cin >> word){
      ++ word2count_map[word];          // 提取word的计数器并将其加1
      // 循环退出条件:输入字符q
      if(word == "q"){
          break;
      }
  }
  // 对map中的每个元素
  for(const auto &w:word2count_map){
      // 打印结果
      cout << w.first << " occurs " << w.second
          << ((w.second > 1) ? " times" : " time") << endl;
  }
}

map遍历

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

int main(){
  map<string, string> book_author_map = {
      {"《书剑恩仇录》", "金庸"},
      {"《在细雨中呐喊》", "余华"},
      {"《穆斯林的葬礼》", "霍达"}
  };
  // 获得一个指向首元素的迭代器
  auto info_iter = book_author_map.cbegin();
  // 比较当前迭代器和尾后迭代器:说明此时还未到尾后元素
  while(info_iter != book_author_map.cend()){
      // 解引用迭代器,打印输出map中的每个元素
      auto book = info_iter->first;
      auto author = info_iter->second;
      cout << "book->author: " << book << "-> " << author << endl;
      // 注意这里的迭代器自增1,移动到下一个迭代器
      ++info_iter;
  }
}

// 输出结果:
book->author: 《书剑恩仇录》-> 金庸
book->author: 《在细雨中呐喊》-> 余华
book->author: 《穆斯林的葬礼》-> 霍达

添加元素

​ insert成员向容器中添加一个元素或一个元素范围。由于map不包含重复的关键字,因此插入一个已经存在的元素对容器没有任何影响。

​ 对一个map进行insert操作时,必须记住元素类型是pair。

​ 通常,对于想要插入的数据,并没有一个现成的pair对象。可以在insert的参数列表中创建一个pair:

// 向word_count插入 word 的4种方法
word_count.insert({word, 1});
word_count.insert(make_pair(word, 1));
word_count.insert(pair<string, size_t>(word, 1));
word_count.insert(map<string, size_t>::value_type(word, 1));

​ 如我们所见,在新标准下,创建一个pair最简单的方法是在参数列表中使用花括号初始化。也可以调用make_pair或显式构造pair。最后一个insert调用中的参数:

map<string, size_t>::value_type(s, 1)

构造一个恰当的pair类型,并构造该类型的一个新对象,插入到map中。

关联容器的insert操作

c.insert(v)				v是value_type类型的对象:arg用来构造一个元素
c.emplace(args)         对于map和set,只有当元素的关键字不在c中时才插入(或构造)元素。函数返回一个pair,包含一个迭代器,指向具有指定关键字的元素,以及一个标识插入是否成功的bool值。
c.insert(b, e)          b和e是迭代器,表示一个c::value_type类型值的范围

检测insert的返回值

insert(或emplace)返回的值依赖于容器类型和参数。对于不包含重复关键字的容器,添加单一元素的insert和emplace版本返回一个pair,告诉我们插入操作是否成功。

pair的first成员是一个迭代器,指向具有给定关键字的元素;second成员是一个bool值,指出元素是插入成功还是存在于容器中。

如果关键字已在容器中,则insert什么事情也不做,且返回值中的bool部分为false。如果关键字不存在,元素被插入容器中,且bool值为true。

示例代码:

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

int main(){
  // 统计每个单词在输入中出现次数的一种更繁琐的方式
  map<string, size_t> word_count;  // 从string到size_t的空map
  string word;
  while(cin >> word){
      if(word == "q"){
          break;
      }
      // 插入一个元素,关键字等于word, 值为1
      // 若word已在word_count中, insert什么也不做
      auto ret = word_count.insert({word, 1});
      if(!ret.second){                // word已在word_count中
          ++ret.first->second;        // 递增计数器
      }
  }
}
// GDB断点调试结果:
(gdb) p ret.first
$1 = {
first = "io", 
second = 1
}
(gdb) p ret
$2 = {
first = {
  first = "io", 
  second = 1
}, 
second = true
}
(gdb) p ret.first
$3 = {
first = "io", 
second = 1
}
(gdb) p ret.first.first
There is no member or method named first.
(gdb) p ret.first->second
$4 = 1
(gdb) p ret.first->first
$5 = "io"

删除元素

关联容器提供一个erase操作,它接受一个key_type参数。此版本删除所有匹配给定关键字的元素(如果存在的话),返回实际删除的元素的数量。示例如下:

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

int main(){
  map<string, string> book_author_map = {
      {"《书剑恩仇录》", "金庸"},
      {"《在细雨中呐喊》", "余华"},
      {"《穆斯林的葬礼》", "霍达"}
  };
  // 删除一个关键字,返回删除的元素数量
  if(book_author_map.erase("《书剑恩仇录》")){
      cout << "OK: " << "《书剑恩仇录》" << " removed\n";
  }
  else{
      cout << "oops: " << "《书剑恩仇录》" << " not found! \n";
  }
}

// GDB调试结果:
bzl@bzl ~/cppcode/Temp o gdb a.out -q
Reading symbols from a.out...done.
(gdb) start
Temporary breakpoint 1 at 0x400fe6: file example.cpp, line 6.
Starting program: /home/bzl/cppcode/Temp/a.out 

Temporary breakpoint 1, main () at example.cpp:6
6	int main(){
(gdb) n
11	    };
(gdb) n
13	    if(book_author_map.erase("《书剑恩仇录》")){
(gdb) n
14	        cout << "OK: " << "《书剑恩仇录》" << " removed\n";
(gdb) p book_author_map
$1 = std::map with 2 elements = {
["《在细雨中呐喊》"] = "余华",
["《穆斯林的葬礼》"] = "霍达"
}
// 可以看到当前的 book_author_map 中已经删除了 《书剑恩仇录》 这个key-value键值对

对于保存不重复关键字的容器,erase的返回值总是0或1。若返回值为0,则表明想要删除的元素并不在容器中。

map的下标操作

map容器提供了下标运算符和一个对应的at函数。

map下标运算符接受一个索引(即,一个关键字),获取与此关键字相关联的值。但是,与其他下标运算符不同的是,如果关键字并不在map中,会为它创建一个元素并插入到map中,关联值将进行初始化:

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

int main(){
  map<string, string> book_author_map = {
      {"《书剑恩仇录》", "金庸"},
      {"《在细雨中呐喊》", "余华"},
      {"《穆斯林的葬礼》", "霍达"}
  };
  // 插入一个关键字为 《黄金时代》
  book_author_map["《黄金时代》"] = "王小波";
  for(auto info_obj:book_author_map){
      cout << info_obj.first << "\n";
      cout << info_obj.second << "\n";
  }
  // at的使用
  auto author = book_author_map.at("《剑恩仇录》");
  cout << author << "\n";
}

由于下标运算符可能插入一个新元素,我们只可以对非const的map使用下标操作。(只能对可变对象执行此操作)

使用一个不在容器中的关键字作为下标,会添加一个具有此关键字的元素到map中。

map的下标操作

c[k]				返回关键字为k的元素:如果k不在c中,添加一个关键字为k的元素,对其进行值初始化
c.at(k)			    访问关键字为k的元素,带参数检查:如果k不在c中,抛出一个out_of_range异常

解引用一个迭代器返回的类型与下标运算符返回的类型是一样的。

对一个map进行下标操作时,会获得一个mapped_type对象;但当解引用一个map迭代器时,会得到一个value_type对象。

与其他下标运算符相同的是,map的下标运算符返回一个左值。由于返回的是一个左值,所以我们既可以读也可以写元素

与vector和string不同,map的下标运算符返回的类型与解引用map迭代器得到的类型不同。

访问元素

下标和at操作只适用于非const的map和unordered_map。

c.find(k)				返回一个迭代器,指向第一个关键字为k的元素,若k不在容器中,则返回尾后迭代器
c.count(k)              返回关键字等于k的元素的数量。对于不允许重复关键字的容器,返回值永远是0或1
c.lower_bound(k)        返回一个迭代器,指向第一个关键字不小于k的元素
c.upper_bound(k)        返回一个迭代器,指向第一个关键字大于k的元素
c.equal_range(k)        返回一个迭代器pair,表示关键字等于k的元素的范围。若k不存在,pair两个成员均等c.end()

对map使用find代替下标操作

​ 下标运算符提供了最简单的提取元素的方法。

​ 但是,如我们所见,使用下标操作有一个严重的副作用:如果关键字还未在map中,下标操作会插入一个具有给定关键字的元素。这种行为是否正确完全依赖于我们的预期是什么。例如,单词计数程序有这样 一个特性:使用一个不存在的关键字作为下标,会插入一个新元素,其关键字为给定关键字,其值为0。也就是说,下标操作的行为符合我们的预期。

​ 但有时,我们只是向知道一个给定关键字是否在map中,而不想改变map。这样就不能使用下标运算符来检查一个元素是否存在,因为如果关键字不存在的话,下标运算符会插入一个新元素。在这种情况下,应该使用find

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

int main(){
    map<string, string> book_author_map = {
        {"《书剑恩仇录》", "金庸"},
        {"《在细雨中呐喊》", "余华"},
        {"《穆斯林的葬礼》", "霍达"}
    };
    // 使用find检查一个元素是否存在
    // 注意:下面的判断条件: 若不在该容器中,find()返回值是尾后迭代器,所以下面的判断逻辑是不存在的时候
    if(book_author_map.find("《书剑恩仇录》") == book_author_map.end()){
        cout << "《雪山飞狐》不存在当前集合中" << endl;
    }
    else{
        cout << "《书剑恩仇录》存在当前集合中" << endl;
    }
}

一个单词转换的map

功能:给定一个string,将它转换为另一个string。程序的输入是两个文件。第一个文件保存的是一些规则,用来转换第二个文件中的文本。每条规则由两部分组成。

规则:一个可能出现在输入文件中的单词和一个用来替换它的短语。表达的含义是:每当第一个单词出现在输入中时,我们就将它替换为对应的短语。第二个输入文件包含要转换的文本。

#include<map>
#include<string>
#include<fstream>
#include<iostream>

using namespace std;

void word_transform(ifstream &map_file, ifstream &input){
    auto trans_map = buildMap(map_file);  // 保存转换规则
    string text;                          // 保存输入中的每一行
    while(getline(input, text)) {         // 读取一行输入
        istringstream stream(text);       // 读取每个单词
        string word;                     
        bool firstword = true;
        while(stream >> word){
            if(firstword){
                firstword = false;
            }
            else{
                cout << " ";
            }
            // transform返回它的第一个参数或其转换之后的形式
            cout << transform(word, trans_map);
        }
        cout << endl;
    }
}

map<string, string> buildMap(ifstream, &map_file){
    map<string, string> trans_map;  // 保存转换规则
    string key;                     // 要转换的单词
    string value;                   // 替换后的内容
    // 读取第一个单词存入key中, 行中剩余内容存入value
    while(map_file >> key && getline(map_file, value)){
        if(value.size() > 1){       // 检查是否有转换规则
            trans_map[key] = value.substr(1);  // 跳过前导空格
        }  
        else{
            throw runtime_error("no rule for " + key);
        }
    }
    return trans_map;
}

const string &transform(const string &s, const map<string, string> &m){
    // 实际的转换工作,此部分是程序的核心
    auto map_item = m.find(s);
    // 如果单词在转换规则中
    if(map_item != m.cend()){
        return map_item->second;  // 使用替换短语
    }
    else{
        return s;                 // 否则返回原string
    }
}

int main(){
    string map_file = "brb be right back
        k okay?
        y why
        r are
        u you
        pic picture
        thk thans!
        18r later
        ";
     // 这个先暂且停一下
     word_transform(map_file, )
}

九、set基本用法

顺序容器包括 vector、deque、list、forward_list、array、string,所有顺序容器都提供了快速访问元素的能力。

关联容器包括 set、map

关联容器不支持顺序容器的位置相关的操作。原因是关联容器中的元素是根据关键字存储的,这些操作对关联容器没有意义。

而且,关联容器也不支持构造函数或插入操作这些接受一个元素值和一个数量值的操作。

关联容器支持高效的关键字查找和访问。两个主要的关联容器类型是map和set。

map中的元素是一些关键字-值对,关键字起索引的作用,值则表示与索引相关的数据。

set中的每一个元素只包含一个关键字:set支持高效的关键字查询操作 -> 检查一个给定的关键字是否在set中。

标准库提供的set关联容器分为:

  • 按关键字有序保存元素:set(关键字即值, 即只保存关键字的容器); multiset(关键字可以重复出现的set)
  • 无序集合:unordered_set(用哈希函数组织的set);unordered_multiset(哈希组织的set,关键字可以重复出现)

set就是关键字的简单集合。当只想知道一个值是否存在时,set是最有用的。

在set中每个元素的值都唯一,而且系统根据元素的值自动进行排序。set中的元素的值不能直接被改变。

set内部采用的是一种非常高效的平衡检索二叉树——红黑树,也称为RB树(Red-Black Tree)。RB树的统计性能要好于一般平衡二叉树。

set具备的两个特点:

  • set中的元素都是排好序的
  • set中的元素都是唯一的,没有重复的

set用法:

初始化方式:
set<T> s  或者 set<T> s(b, e)  
	说明: 其中,b 和 e 分别为迭代器开始和结束的标记(其中的参数b大多数情况下是数组)

begin();            // 返回指向第一个元素的迭代器

end();              // 返回指向迭代器的最末尾处(即最后一个元素的下一个位置)

clear();            // 清除所有元素

count();            // 返回某个值元素的个数

empty();            // 如果集合为空,返回true

equal_range();      // 返回集合中与给定值相等的上下限的两个迭代器

erase()–删除集合中的元素

find()–返回一个指向被查找到元素的迭代器

get_allocator()–返回集合的分配器

insert()–在集合中插入元素

lower_bound()–返回指向大于(或等于)某值的第一个元素的迭代器

key_comp()–返回一个用于元素间值比较的函数

max_size()–返回集合能容纳的元素的最大限值

rbegin()–返回指向集合中最后一个元素的反向迭代器

rend()–返回指向集合中第一个元素的反向迭代器

size()–集合中元素的数目

swap()–交换两个集合变量

upper_bound()–返回大于某个值元素的迭代器

value_comp()–返回一个用于比较元素间的值的函数

示例代码:

// begin() 和 end() 的使用
#include<iostream>
#include<set>
using namespace std;

int main(){
    // 先定义一个整型数组
    int int_array[] = {75, 23, 65, 42, 13, 13, 5, 2, 745, 412, 143};
    // 迭代器开始的位置 int_array 就是数组首元素的地址
    // 迭代器结束的位置 int_array + 5 就是右移5个位置的地址
    // set<int> int_set(int_array, int_array + 5);
    // 我们换个写法: sizeof(int_array) / sizeof(*int_array) 的意思: 数组占据内存空间大小/数组首元素占据空间大小 = 有几个元素, 即数组的元素个数
    set<int> int_set(int_array, int_array + sizeof(int_array) / sizeof(*int_array));
    cout << "set contains: ";
    for(auto item = int_set.begin(); item != int_set.end(); ++item){
        cout << " " << *item;  // 解引用获取到item指向的对象值
    }
    cout << "\n";
}

// 输出结果:
// set contains:  2 5 13 23 42 65 75 143 412 745

// GDB调试结果:
(gdb) p int_set
$1 = std::set with 10 elements = {
  [0] = 2,
  [1] = 5,
  [2] = 13,
  [3] = 23,
  [4] = 42,
  [5] = 65,
  [6] = 75,
  [7] = 143,
  [8] = 412,
  [9] = 745
}
(gdb) *int_array
Undefined command: "".  Try "help".
(gdb) p *int_array
$2 = 75
(gdb) call(sizeof(*int_array))
$3 = 4
(gdb) call(sizeof(int_array))
$4 = 44

十、读写文件

C++语言不直接处理输入输出,而是通过一组定义在标准库中的类型类处理IO。这些类型支持从设备读取数据、向设备写入数据的IO操作,设备可以是文件、控制台窗口等。

IO库定义了读写内置类型值的操作。此外,一些类,如string,通常也会定义类似的IO操作,来读写自己的对象。

IO对象无拷贝或赋值

我们不能拷贝或者对IO对象赋值:

ofstream out1, out2;
out1 = out2;                      // 错误:不能对流对象赋值
ofstream print(ofstream);         // 错误:不能初始化ofstream参数
out2 = print(out2);               // 错误:不能拷贝流对象

由于不能拷贝IO对象,因此我们也不能将形参或返回类型设置为流类型。进行IO操作的函数通常以引用的方式传递和返回流。读写一个IO对象会改变其状态,因此传递和返回的引用不能是const的。

管理输出缓冲

执行下面的代码:

os << "please enter a value: ";

​ 文本串可能立即打印出来,但也有可能被操作系统保存在缓冲区中(即系统来不及进行处理,先存在自己的缓存区),随后再打印。有了缓冲机制,操作系统就可以将程序的多个输出操作组合成单一的系统级写操作。由于设备的写操作可能很耗时,允许操作系统将多个输出操作组合为单一的设备写操作可以带来很大的提升。

​ 导致缓冲刷新(即,数据真正写到输出设备或文件)的原因有很多:

  • 程序正常结束,作为main函数的return操作的一部分,缓冲刷新被执行。
  • 缓冲区满时,需要刷新缓冲,而后新的数据才能继续写入缓冲区。
  • 我们可以使用操作符如endl来显式刷新缓冲区。
  • 在每个输出操作后,我们可以用操作符unitbuf设置流的内部状态,来清空缓冲区。默认情况下,对cerr是设置unitbuf的,因此写到cerr的内容是立即刷新的。
  • 一个输出流可能被关联到另一个流。在这种情况下,当读写被关联的流时,关联的流的缓冲区会被刷新。例如,默认情况下,cin和cerr都关联到cout。因此,读cin或写cerr都会导致cout的缓冲区被刷新。

刷新输出缓冲区

endl:换行,且刷新缓冲区

flush:刷新缓冲区,不输出任何额外的字符

ends:向缓冲区插入一个空字符,然后刷新缓冲区

unitbuf:每次输出操作后都刷新缓冲区,它告诉流在接下来的每次写操作之后都进行一次flush操作。

nounitbuf:重置流,使其恢复使用正常的系统管理的缓冲区刷新机制。

注意

​ 如果程序崩溃,输出缓冲区不会被刷新。

​ 当一个程序崩溃后,它所输出的数据很可能停留在输出缓冲区等待打印。

​ 当调试一个已经崩溃的程序时,需要确认那些你认为已经输出的数据确实已经刷新了。否则,可能将大量时间浪费在追踪代码为什么没有执行上,而实际上代码已经执行了。只是程序崩溃后缓冲区没有被刷新,输出数据被挂起没有打印而已。

​ 当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新新关联的输出流。标准库将cout和cin关联在一起,因此下面语句:

cin >> ival;

导致cout的缓冲区被刷新。

​ 交互式系统通常应该关联输入流和输出流。这意味着所有输出,包括用户提示信息,都会在读操作之前被打印出来。

文件输入输出

​ 头文件ofstream定义了三个类型来支持文件IO:

  • ifstream 从一个给定文件读取数据
  • ofstream 向一个给定文件写入数据
  • fstream 读写指定文件

我们可以用IO运算符(<< 和 >>) 来读写文件,可以用getline 从一个ifstream读取数据。

示例代码:

// 写入文件内容
#include<fstream>
#include<iostream>
using namespace std;

int main(){
    // 输入到文件
    ofstream file_obj("out.txt");
    if(!file_obj){
        cerr << "error in out" << endl;
    }
    else{
        cerr << "successfully open out file" << endl;
        file_obj << "Hello World !!!" << endl;
    }
    ofstream file_append_obj("out.txt", ios_base::app);
    if(!file_append_obj){
        cerr << "error int out_with_append file" << endl;
    }
    else{
        cerr << "successfully open out_with_append file" << endl;
        file_append_obj << "Hello World again!" << endl;
        file_append_obj << "这是新的时代,未来是你的" << endl;
    }
}
// 输出结果:
// 当前目录下生成out.txt文件
// 文件中生成上述文本内容

从文件中读入

示例代码:

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

int main(){
    // 写入文件内容
    ofstream file_obj("out.txt");
    if(!file_obj){
        cerr << "error in out" << endl;
    }
    else{
        cerr << "successfully open out file" << endl;
        file_obj << "Hello World!" << endl;
    }
    ofstream file_append_obj("out.txt", ios_base::app);
    if(!file_append_obj){
        cerr << "error in file_append_obj" << endl;
    }
    else{
        cerr << "successfully open file_append_obj file" << endl;
        file_append_obj << "Hello World 李斌!!!" << endl;
    }
    // 从文件中读取
    ifstream read_file_obj("out.txt");
    if(!read_file_obj){
        cerr << "error in read_file_obj" << endl;
    }
    else{
        cerr << "successfully open read_file_obj file" << endl;
        string read_buf_obj;
        while( read_file_obj >> read_buf_obj){
            cout << read_buf_obj << endl;
        }
    }
}

追加模式作用

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

int main(){
    // 写入文件内容
    ofstream file_obj("out.txt");
    if(!file_obj) {
        cerr << "error in out" << endl;
    }
    else {
        cerr << "successfully open out file" << endl;
        file_obj << "Hello World!" << endl;
    }
    ofstream file_append_obj("out.txt", ios_base::app);
    if(!file_append_obj) {
        cerr << "error in file_append_obj" << endl;
    }
    else {
        cerr << "successfully open file_append_obj file" << endl;
        file_append_obj << "Hello World!!!" << endl;
    }
    // 从文件中读取
    ifstream read_file_obj("out.txt", ios_base::in | ios_base::app);
    read_file_obj.seekg(0);
    // 当读到文件末尾的时候会返回false,因此可以在while的循环表达式中作为结束的标志。
    if(!read_file_obj) {
        cerr << "error in read_file_obj" << endl;
    }
    else {
        cerr << "successfully open read_file_obj file" << endl;
        string buf_obj;
        while(read_file_obj >> buf_obj){
            cout << buf_obj;
        }
        // 输出换行符并刷新缓冲区
        cout << endl;
        // 是这里的问题:是需要在这个文件中添加进去新的文本内容
        file_append_obj << "append text" << endl;
        read_file_obj >> buf_obj;
        cout << buf_obj << endl;
    }
    return 0;
}

在新C++标准中,文件名既可以是库类型string对,也可以是C风格字符数组。旧版本的标准库只允许C风格字符数组。

十一、读写json文件

需要依赖第三方库:jsoncpp

官方文档:

http://open-source-parsers.github.io/jsoncpp-docs/doxygen/index.html

https://github.com/open-source-parsers/jsoncpp

安装步骤:

  • 下载 jsoncpp-1.9.4.tar.gz 这个包,下载地址 https://github.com/open-source-parsers/jsoncpp/releases

  • cd jsoncpp-1.9.4;mkdir -p build/debug;cd build/debug;cmake -DCMAKE_BUILD_TYPE=release -DBUILD_STATIC_LIBS=OFF -DBUILD_SHARED_LIBS=ON -DARCHIVE_INSTALL_DIR=. -DCMAKE_INSTALL_INCLUDEDIR=include -G "Unix Makefiles" ../..;make && sudo make install

  • 包含头文件的json文件夹位于 /usr/local/include,库文件位于 /usr/local/lib

  • 打开 /etc/profile,添加下面两行:

    export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib

    export LIBRARY_PATH=$LIBRARY_PATH:/usr/local/lib

从字符串中读取json数据

示例代码:

#include <string>
#include <json/json.h>
#include <iostream>
#include <fstream>
#include <typeinfo>
#include <vector>

using namespace std;

// 先声明函数
void readStrJson();            // 从字符串中读取json数据(简单模式)
void readStrProJson();         // 从字符串中读取json数据(复杂模式)

int main(int argc, char *argv[]){
    cout << "简单情况: " << endl;
    readStrJson();
    cout << "复杂情况: " << endl;
    readStrProJson();
    return 0;
}


void readStrJson(){
    // 字符串: 注意使用 "" 也可以
    const char *str = "{\"praenomen\":\"Gaius\","
      "\"nomen\":\"Julius\",\"cognomen\":\"Caezar\","
      "\"born\":-100,\"died\":-44}";  

    /* 
    json内容如下: key:val其中val为字符串、数字等基础类型
    { 
        "praenomen":"Gaius", 
        "nomen":"Julius", 
        "cognomen":"Caezar", 
        "born":-100, 
        "died":-44  
    } 
    */
    
    Json::CharReaderBuilder rbuilder;  // 用于读取字符串
    Json::Value root;     // 用于存放到map中
    
    string praenomen = root["praenomen"].asString();
    string nomen = root["nomen"].asString();
    string cognomen = root["cognomen"].asString();
    int born = root["born"].asInt();
    int died = root["died"].asInt();
    cout << praenomen + " " + cognomen << " was born " << born
        << ", died in year " << died << endl;
    
}

void readStrProJson(){
    string strValue = "{\"name\":\"json\",\"array\":[{\"cpp\":\"jsoncpp\"},{\"java\":\"jsoninjava\"},{\"php\":\"support\"}]}";  
     /* 
      json内容如下: 
    { 
    "name": "json″, 
    "array": [ 
        { 
            "cpp": "jsoncpp" 
        }, 
        { 
            "java": "jsoninjava" 
        }, 
        { 
            "php": "support" 
        } 
    ] 
    } 
    */  
    Json::CharReaderBuilder rbuilder;
    Json::Value root;
    
    string name = root["name"].asString();
    cout << name << endl;
    const Json::Value arrayObj = root["array"];

    for(unsigned int i = 0; i < arrayObj.size(); ++i){
        if(arrayObj[i].isMember("cpp")){  // 查找内部是否有这个key
            cout << "key is cpp" << endl;
        }
        vector<string> mems = arrayObj[i].getMemberNames();  // 获取所有keys
        for(unsigned int j = 0; j < mems.size(); ++j){
            cout << mems[j] << ":" << arrayObj[i][string(mems[j])];
        }
    }
}

生成Json字符串:

#include <json/json.h>
#include <iostream>
#include <string>
using namespace std;

string genJson(){
    string jsonStr;
    // 定义root、lang、mail 三个对象,三者都是 Value类型
    Json::Value root, lang, mail;
    Json::StreamWriterBuilder wBuilder;
	// 文件流对象 os_obj
    ostringstream os_obj;
    
    root["Name"] = "LiNing";
    root["Age"] = 26;
    
    lang[0] = "C++";
    lang[1] = "Java";
    root["language"] = lang;
    
    mail["Netease"] = "bngzifei89@163.com";
    mail["gmail"] = "bngzifei89@gmail.com";
    root["mail"] = mail;
    
    unique_ptr<Json::StreamWriter> jsonWriterPtr(wBuilder.newStreamWriter());
    // jsonWriterPtr 是个指针,指针解引用后获得指向的对象。然后对象.调用成员函数 (*jsonWriterPtr).成员函数
    jsonWriterPtr->write(root, &os_obj);
    jsonStr = os_obj.str();
    
    cout << "Json: \n" << jsonStr << endl;
    return jsonStr;
    
}


int main(int argc, char *argv[]){
    genJson();
}

安装jsoncpp遇到的问题:

https://www.e-learn.cn/topic/3830619

https://blog.csdn.net/shaosunrise/article/details/84680602

https://www.cnblogs.com/ssyfj/p/14011014.html

解析Json字符串:

#include <json/json.h>
#include <iostream>
#include <string>
using namespace std;

// 声明函数
bool parseJson(const string &info);
string genJson();

int main() {
    string info = genJson();
    // 调用函数:注意这里不需要指定入参的类型,传参数就行
    cout << "parse json ... "<< endl;
    parseJson(info);
}


bool parseJson(const string &info){
    if(info.empty()){
        return false;
    }
    bool res;
    JSONCPP_STRING errs;
    Json::Value root, lang, mail;
    Json::CharReaderBuilder readBuilder;
    
    unique_ptr<Json::CharReader> const jsonReader(readBuilder.newCharReader());
    res = jsonReader->parse(info.c_str(), info.c_str() + info.length(), &root, &errs);
    if(!res || !errs.empty()){
        cout << "parseJson err" << errs << endl;
    }
    cout << "Name: " << root["Name"].asString() << endl;
    cout << "Age: " << root["Age"].asString() << endl;
    
    lang = root["Language"];
    cout << "Language: ";
    for(int i = 0; i < lang.size(); ++i){
        cout << lang[i] << " ";
    }
    cout << endl;
    
    mail = root["E-mail"];
    cout << "Netease: " << mail["Netease"].asString() << endl;
    cout << "Hotmail: " << mail["Hotmail"].asString() << endl;
    cout << "Gmail: " << mail["Gmail"].asString() << endl;
    return true;
}

string genJson(){
    string jsonStr;
    // 定义root、lang、mail 三个对象,三者都是 Value类型
    Json::Value root, lang, mail;
    Json::StreamWriterBuilder wBuilder;
	// 文件流对象 os_obj
    ostringstream os_obj;
    
    root["Name"] = "LiNing";
    root["Age"] = 26;
    
    lang[0] = "C++";
    lang[1] = "Java";
    root["Language"] = lang;
    
    mail["Netease"] = "bngzifei89@163.com";
    mail["Hotmail"] = "bngzifei89@hotmail.com";
	mail["Gmail"] = "bngzifei89@gmail.com";    
    root["E-mail"] = mail;
    
    unique_ptr<Json::StreamWriter> jsonWriterPtr(wBuilder.newStreamWriter());
    // jsonWriterPtr 是个指针,指针解引用后获得指向的对象。然后对象.调用成员函数 (*jsonWriterPtr).成员函数
    jsonWriterPtr->write(root, &os_obj);
    jsonStr = os_obj.str();
    
    cout << "Json: \n" << jsonStr << endl;
    return jsonStr;
    
}
// 关于cmake的讨论:
// https://zhuanlan.zhihu.com/p/103219038

使用CMake工具进行编译:

# 设置CMake最小版本
cmake_minimum_required(VERSION 3.2.1)
# 设置工程名
project(example-test)
# 生成可执行文件
add_executable(example-test example.cpp)


# 直接指定编译时需要链接的第三方库名称:jsoncpp 等价于编译命令:g++ example.cpp -ljsoncpp
target_link_libraries(example-test jsoncpp)


# 下面的设置是指定使用 C++11 标准进行编译
include(CheckCXXCompilerFlag)
CHECK_CXX_COMPILER_FLAG("-std=c++11" COMPILER_SUPPORTS_CXX11)
if(COMPILER_SUPPORTS_CXX11)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
elseif(COMPILER_SUPPORTS_CXX0X)
     set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x")
else()
    message(STATUS "The compiler ${CMAKE_CXX_COMPILER} has no C++11 support. Please use a different C++ compiler.")
    
endif()

编译执行步骤:

1、进入 example.cpp 文件所在目录,执行 mkdir build 创建 build 目录。

2、cd build 进入 build 目录下,执行 cmake ../

3、继续执行 make 后生成可执行文件

注意:

1、在指定目录下创建include 目录,里面存放的是 .h 头文件,用于导包

2、在指定目录下创建src 目录,里面存放的是 .cpp 源文件,是各个成员函数的具体实现

3、在指定目录下创建CMakeLists.txt,里面设置的内容就是如何将 .h.cpp 文件串联起来,以及指定编译器的标准、需要链接的三方库等等

十一、CMake常用指令

cmake_minimum_required(VERSION 3.6)

// 项目名称

project(BoostCoroutineDemo)

// c++标准

set(CMAKE_CXX_STANDARD 11)

// 指定生成的版本

set(CMAKE_BUILD_TYPE DEBUG)

// 指定编译选项

set(CMAKE_CXX_FLAGS_DEBUG “-g -Wall”)

// 指定源代码

set(SOURCE_FILES main.cpp)

// 指定头文件目录

include_directories(“/usr/local/boost-1.57/include”)

// 指定静态和动态文件目录

link_directories(“/usr/local/boost-1.57/lib”)

// 生成目标文件

add_executable(BoostCoroutineDemo ${SOURCE_FILES})

// 链接库文件

target_link_libraries(BoostCoroutineDemo libboost_system.a libboost_thread.a)

target_link_libraries(BoostCoroutineDemo pthread)

CMakeLists.txt 包含头文件实际使用解释说明:

# 最低CMake版本
cmake_minimum_required(VERSION 3.5)

# 工程名
project (hello_headers)

# 创建一个变量,名字叫SOURCE。它包含了所有的cpp文件,即SOURCES变量的值就是下面的 src/xxx.cpp
set(SOURCES
    src/Hello.cpp
    src/main.cpp
    )

# 用所有的源文件生成一个可执行文件,因为这里定义了SOURCES变量,所以就不需要罗列cpp文件了
# 等价于命令:add_executable(hello_headers src/Hello.cpp src/main.cpp)
add_executable(hello_headers ${SOURCES})

# 设置这个可执行文件hello_headers 需要包含的库的路径
# PRIVATE:指定包含的范围
target_include_directories(hello_headers
	PRIVATE 
    ${PROJECT_SOURCE_DIR}/include
)
# PROJECT_SOURCE_DIR:指工程顶层目录
# PROJECT_Binary_DIR:指编译目录

CMakeLists.txt 包含静态库实际使用解释说明:

cmake_minimum_required(VERSION 3.5)
project(hello_library)
############################################################
# Create a library
############################################################
# 库的源文件Hello.cpp生成静态库hello_library
# add_library()函数用于从某些源文件创建一个库,默认生成在构建文件夹
# 在add_library调用中包含了源文件,用于创建名称为libhello_library.a 的静态库
add_library(hello_library STATIC 
    src/Hello.cpp
)
    
    
# 使用target_include_directories()添加了一个目录,这个目录是库所包含的头文件的目录,并设置库属性为PUBLIC
# 使用这个函数后,这个目录会在以下情况被调用:
# 1、编译这个库的时候: 因为这个库hello_library由Hello.cpp生成,Hello.cpp中函数的定义(即声明)在Hello.h中,Hello.h 在这个include目录下,所以显然编译这个库的时候,这个目录会用到
# 2、编译链接到这个库hello_library的任何其他目标(库或者可执行文件)
target_include_directories(hello_library
    PUBLIC
    ${PROJECT_SOURCE_DIR}/include
)
# target_include_directories 为一个目标(可能是一个库library也可能是可执行文件)添加头文件路径。
############################################################
# Create an executable
############################################################
# Add an executable with the above sources
# 指定用哪个源文件生成可执行文件
add_executable(hello_binary 
    src/main.cpp
)
# 链接可执行文件和静态库
target_link_libraries(hello_binary
    PRIVATE 
    hello_library
)
# 链接库和包含头文件都有关于scope这三个关键字的用法

private public interface 的范围详解

  • private:目录被添加到目标(库)的包含路径中
  • interface:目录没有被添加到目标(库)的包含路径中,而是链接了这个库的其他目标(库或者可执行程序)包含路径中
  • public:目录既被添加到目标(库)的包含路径中,同时添加到了链接了这个库的其他目标(库或者可执行程序)的包含路径中

​ 也就是说,根据库是否包含这个路径,以及调用了这个库的其他目标是否包含这个路径,可以分为三种scope。

建议:

对于公共的头文件,最好在include文件夹下建立子目录。

传递给函数 target_include_directories()的目录,应该是所有包含目录的根目录,然后在这个根目录下建立不同的文件夹,分别写头文件。

这样使用的时候,不需要写${PROJECT_SOURCE_DIR}/include,而是直接选择对应的文件夹里对应头文件。下面是例子:

include “static/Hello.h”

而不是

include “Hello.h”

使用此方法意味着在项目中使用多个库时,头文件名冲突的可能性较小。

链接库

创建将使用这个库的可执行文件时,必须告知编译器需要用到这个库。可以使用target_link_libaray()函数完成此操作。

add_executable()链接源文件,target_link_libraries()链接库文件。

add_executable(

​ hello_binary

​ src/main.cpp

)

target_link_libraries(

​ hello_binary

​ PRIVATE

​ hello_library

)

这告诉CMake在链接期间将hello_library链接到hello_binary可执行文件。同时,这个被链接的库如果有INTERFACE或者PUBLIC属性的包含目录,那么,这个包含目录页会被传递propagate给这个可执行文件。

CMakeLists.txt 包含动态库实际使用解释说明:

cmake_minimum_required(VERSION 3.5)
project(hello_library)
############################################################
# Create a library
############################################################
# 根据Hello.cpp生成动态库
# add_library() 函数用于从某些源文件(即src/xx.cpp)创建一个动态库
# 在add_library()调用中包含了源文件,用于创建名称为libhello_library.so的动态库
add_library(hello_library SHARED 
    src/Hello.cpp
)

# 顾名思义,别名目标是在只读上下文中可以代替真实目标名称的替代名称
# 如下所示,当你将目标链接到其他目标时,使用别名可以引用目标
# 给动态库hello_library起一个别的名字hello::library
add_library(hello::library ALIAS hello_library)


# 为这个库目标,添加头文件路径,PUBLIC表示包含了这个库的目标也会包含这个路径
# 意思就是这个 .h 文件存放的位置要加进来,以便在其他.cpp文件导包的时候能找到
target_include_directories(hello_library
    PUBLIC 
        ${PROJECT_SOURCE_DIR}/include
)
############################################################
# Create an executable
############################################################
# 根据main.cpp生成可执行文件
add_executable(hello_binary
    src/main.cpp
)

# 创建可执行文件时,请使用target_link_libaray()函数指向你的库
# 这告诉CMake使用别名目标名称将hello_library链接到hello_binary可执行文件
# 链接库和可执行文件,使用的是这个库的别名。PRIVATE表示
target_link_libraries(hello_binary
    PRIVATE 
       hello::library
)

设置构建类型

cmake_minimum_required(VERSION 3.5)
# 如果没有指定则设置默认编译方式
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
  # 在命令行中输出message里的信息
  message("Setting build type to 'RelWithDebInfo' as none was specified.")
  # 不管CACHE里有没有设置过CMAKE_BUILD_TYPE这个变量,都强制赋值这个值为RelWithDebInfo
  set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build." FORCE)

  # 当使用cmake-gui的时候,设置构建级别的四个可选项
  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release"
    "MinSizeRel" "RelWithDebInfo")
endif()

project (build_type)
add_executable(cmake_examples_build_type main.cpp)

CMake具有许多内置的构建配置,可以用于编译工程。这些配置指定了代码优化的级别,以及调试信息是否包含在二进制文件中。

这些优化级别,主要有:

  • Release——不可以打断点调试,程序开发完成后发行使用的版本,占的体积小。它对代码做了优化,因此速度会非常快。在编译器中使用命令:-03-DNDEBUG可选择此版本。
  • Debug——调试的版本,体积大。在编译器中使用命令:-g 可选择此版本。
  • MinSizeRel——最小体积版本。在编译器中使用命令:-Os -DNDEBUG 可选择此版本。
  • RelWithDebInfo——既优化又能调试。在编译器中使用命令:-O2 -g -DNDEBUG 可选择此版本。

设置级别的方式

  • CMake图形界面

  • CMake命令行中

    在命令行运行CMake的时候,使用cmake命令行的-D选项配置编译类型:

    cmake .. -DCMAKE_BUILD_TYPE=Release
    
  • CMake中设置默认的构建级别

    CMake提供的默认构建类型是不进行优化的构建级别。对于某些项目,需要自己设置默认的构建类型,以便不必记住进行设置

  • set()命令

    set(<variable> <value>... [PARENT_SCOPE])
    

    该命令可以为普通变量、缓存变量、环境变量赋值

    设置的变量值作用域属于整个CMakeLists.txt文件(一个工程可能有多个CMakeLists.txt文件)

设置编译方式

编译标志:即编译选项,可执行文件的生成离不开编译和链接,那么如何编译?比如编译时使用C++的哪一个标准?

这些编译设置都在CMAKE_CXX_FLAGS变量中(C语言编译选项是CMAKE_C_FLAGS)

CMakeLists.txt 设置编译方式的解释说明:

cmake_minimum_required(VERSION 3.5)
# 强制设置默认C++编译标志变量为缓存变量,如CMake(五) build type所说,该缓存变量被定义在文件中,相当于全局变量,源文件中也可以使用这个变量
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DEX2" CACHE STRING "Set C++ Compiler Flags" FORCE)
project (compile_flags)

add_executable(cmake_examples_compile_flags main.cpp)
# 为可执行文件添加私有编译定义
target_compile_definitions(cmake_examples_compile_flags 
    PRIVATE EX3
)
# 命令的具体解释在二  CMake解析中,这里的注释只说明注释后每一句的作用

设置每个目标编译标志

默认的CMAKE_CXX_FLAGS为空或包含适用于构建类型的标志。要设置其他默认编译标志,如下使用:

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DEX2" CACHE STRING "Set C++ Compiler Flags" FORCE)

强制设置默认C++编译标志变量为缓存变量,该变量被定义在文件中,相当于全局变量,源文件中也可以使用这个变量。这个变量原本包含的参数仍然存在,只是添加了EX2。

包含第三方库

cmake_minimum_required(VERSION 3.5)

# Set the project name
project (third_party_include)
# find a boost install with the libraries filesystem and system
# 使用库文件系统和系统查找boost install
find_package(Boost 1.46.1 REQUIRED COMPONENTS filesystem system)
# 这是第三方库,而不是自己生成的静态动态库
# check if boost was found
if(Boost_FOUND)
    message ("boost found")
else()
    message (FATAL_ERROR "Cannot find Boost")
endif()

# Add an executable
add_executable(third_party_include main.cpp)

# link against the boost libraries
target_link_libraries(third_party_include
    PRIVATE
        Boost::filesystem
)

使用clang编译工程

#!/bin/bash
# pre_test脚本删除之前配置的build文件,run_test运行clang,生成这次的build.clang文件
# 这个脚本的作用是如果存在build.clang这个文件夹,就把它删除掉

ROOT_DIR=`pwd` # shell脚本的语法,pwd 输出文件当前所在路径,赋值给ROOT_DIR这个变量
dir = "01-basic/I-compiling-with-clang"
if [ -d "$ROOT_DIR/$dir/build.clang" ]; then
    echo "deleting $dir/build.clang"
    rm -r $dir/build.clang
fi


# if then fi 是shell脚本里的判断语句,如果[]里的条件为真,则执行then后面的语句
# 基本格式:
#        if [判断语句]; then
#            执行语句
#        fi
# -d与路径配合,路径存在则为真
# 单纯的dir等价于ls -C -b; 也就是说,默认情况下,文件在列中列出,并垂直排序,特殊字符由反斜杠转义序列表示。
# 也就是说只要当前历经下存在build.clang就删除掉
# 本文dir是一个变量

指定C++标准

# 设置CMake最小版本
cmake_minimum_required(VERSION 3.2.1) 
# 设置工程名
project(hello_cmake) 
# 生成可执行文件
add_executable(example-test example.cpp)

include(CheckCXXCompilerFlag)
CHECK_CXX_COMPILER_FLAG("-std=c++11" COMPILER_SUPPORTS_CXX11)
if(COMPILER_SUPPORTS_CXX11)#
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
elseif(COMPILER_SUPPORTS_CXX0X)#
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x")
else()
    message(STATUS "The compiler ${CMAKE_CXX_COMPILER} has no C++11 support. Please use a different C++ compiler.")
endif()

C++实现单例模式

请用C++ 写一个单例,考虑一下多线程环境?

这是一个常见的面试题。

这个问题可以很简单,也可以很复杂。

简单有效的单例

class Singleton {
    public:
    	static Singleton* GetInstance() {
            Singleton singleton;
            return &singleton;
        }
};
// 在C++11中静态局部变量的初始化是线程安全的。
// 这种写法既简单,又是线程安全的,可以满足大多数场景的需求。

饿汉模式

// 单例在程序初期进行初始化,即无论如何都会初始化
class Singleton {
    public:
    	static Singleton* GetInstance() {
            return singleton;
        }
    static Singleton* singleton;
};

Singleton* Singleton::singleton = new Singleton();

这种写法也是线程安全的,不过Singleton 的构造函数在main函数之前执行,有些场景下是不允许这么做的。改进一下:

class Singleton {
    public:
    	static Singleton* GetInstance() {
            return singleton;
        }
    	int Init();
	    static Singleton* singleton;
};
Singleton* Singleton::singleton = new Singleton();

将复杂的初始化操作放在Init函数中,在主线程中调用。

懒汉模式

单例在首次调用时进行初始化

class Singleton {
    public:
    	static Singleton* GetInstance() {
            if(singleton == NULL ) {
                singleton = new Singleton();
            }
            return singleton;
        }
    static Singleton* singleton;
};
Singleton* Singleton::singleton = NULL;

这样写不是线程安全的。改进一下:

class Singleton {
    public:
    	static Singleton* GetInstance() {
            lock();
            if(singleton == NULL){
                singleton = new Singleton();
            }
            unlock();
            return singleton;
        }
    static Singleton* singleton;
};
Singleton* Singleton::singleton = NULL;

这样写虽然是线程安全的,但每次都要加锁会影响性能。

在懒汉模式的基础上再改进一下:

class Singleton {
    public:
    	static Singleton* GetInstance() {
            if(singleton == NULL) {
                lock();
                if(singleton == NULL){
                    singleton = new Singleton();
                }
                unlock();
            }
            return singleton;
        }
    static Singleton* singleton;
};
Singleton* Singleton::singleton = NULL;

两次if判断避免了每次都要加锁。但是,这样仍是不安全的。因为 singleton = new Singleton(); 这句不是原子的。

这句可以分为三步:

  1. 申请内存
  2. 调用构造函数
  3. 将内存指针赋值给 singleton

上面这个顺序是我们期望的,但是编译器并不会保证这个执行顺序。所以也有可能是按下面这个顺序执行的:

  1. 申请内存
  2. 将内存指针赋值给singleton
  3. 调用构造函数

这样就会导致其他线程可能获取到未构造好的单例指针。

解决办法:

class Singleton {
    public:
    	static Singleton* GetInstance() {
            if(singleton == NULL) {
                lock();
                if(singleton == NULL){
                    Singleton* tmp = new Singleton();
                    memory_barrier();  // 内存屏障
                    singleton = tmp;
                }
                unlock();
            }
            return singleton;
        }
    static Singleton* singleton;
};
Singleton* Singleton::singleton = NULL;

语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。简单的说就是保证指令一定程度上的按顺序执行,避免上述所说的乱序行为。

返回指针还是返回引用?

Singleton 返回的实例的生存期是由 Singleton 本身所决定的,而不是用户代码。我们知道,指针和引用在语法上的最大区别就是指针可以为NULL,并可以通过delete 运算符删除指针所指的实例对象,而引用则不可以。由该语法区别引申出的语义区别之一就是这些实例的生存期意义:通过引用所返回的实例,生存期由非用户代码管理,而通过指针返回的实例,其可能在某个时间点没有被创建,或是可以被删除的。但是这两条Singleton都不满足,所以返回引用更好一些。

class Singleton {
    public:
    	static Singleton& GetInstance() {
            static Singleton singleton;
            return singleton;
        }
    	// 如果需要有比较重的初始化操作,则在安全的情况下初始化
    	int Init();
    private:
    	// 禁用构造函数、拷贝构造函数、拷贝函数
    	Singleton();
    	Singleton(const Singleton&);
    	Singleton& operator=(const Singleton&);
};

这种写法比较简单,可以满足大多数场景的需求。如果不能满足需求,再考虑DCLP那种复杂的模式。

附录:

不错的C++学习博客:

https://www.coonote.com/cplusplus-note

https://interview.huihut.com/#/

https://github.com/huihut/interview

关于单测的讨论:

https://www.zhihu.com/question/27313846

Gmock相关资料:

http://google.github.io/googletest/

https://zhangyuyu.github.io/cpp-unit-test/#2-特别鸣谢

https://gohalo.me/post/cpp-gmock-usage.html

https://github.com/google/googletest/releases/tag/release-1.11.0

https://my.oschina.net/u/3485339/blog/900443

https://www.jianshu.com/p/356ce11a8d08

https://blog.csdn.net/songqier/article/details/79188237

https://www.cnblogs.com/jycboy/p/gmock_summary.html

https://www.cnblogs.com/coderzh/archive/2009/04/06/1426755.html

台湾的博客:

https://code-examples.net/zh-TW/q/ce52d2#header

大神修改的Imock:

https://github.com/wangyongfeng5/lmock

https://zhuanlan.zhihu.com/p/379605663

Java工具类库:

https://www.hutool.cn/docs/#/

RustPython:

https://github.com/RustPython/RustPython

https://rustpython.github.io/

GDB使用:

http://witmax.cn/gdb-usage.html

https://zhuanlan.zhihu.com/p/74897601

GitHub开源广场:

https://codechina.csdn.net/mirrors/justjavac/replacegooglecdn?utm_source=csdn_github_accelerator

C++编译期的类型测试:

https://blog.csdn.net/wcyoot/article/details/32963193

高性能服务器开发:

https://balloonwj.github.io/cpp-guide-web/

自动化测试工具:Parasoft C/C++test?

https://blog.csdn.net/Juvien_Huang/article/details/84030965

C++单元测试打桩技巧:

https://blog.csdn.net/coolxv_6533/article/details/79550197

好用的命令行神器:

https://www.zhihu.com/question/59227720/answer/286665684?utm_source=wechat_session

cmake学习:

https://sfumecjf.github.io/cmake-examples-Chinese/

https://github.com/ttroy50/cmake-examples

catch2测试框架:

https://github.com/catchorg/Catch2

cpp-stub:

https://github.com/coolxv/cpp-stub

CppUnit单测使用:

https://www.cnblogs.com/hanerfan/p/4787495.html

git合并分支的四种方法:

https://zhuanlan.zhihu.com/p/28137908

json官方组织:

https://www.json.org/json-zh.html

C++中json库如何选择:

https://www.zhihu.com/question/23654513

PS:本地存储代码的名字建议其名为:GitRepo,这样显得更专业一点

posted @ 2021-09-07 21:03  砚台是黑的  阅读(56)  评论(0编辑  收藏  举报