ZhangJie Software Development Engineer

C++基础知识查漏补缺

2021-02-02
ZhangJie

C++基础知识查漏补缺。

输入输出流

流(stream):随着时间的推移,字符是顺序生成或消耗的。

输入运算符(“»”)/输出运算符(“«“)返回其左侧的运算对象:

std::cout << "hello world!" << std::endl;

// 等价于:
(std::cout << "hello world!") << std::endl;

写入endl的效果是结束当前行,并将与设备关联的缓冲区中的内容刷到设备中。

当使用一个istream对象作为条件时,其效果是检测流的状态。如果流是有效的,即流未遇到错误,则检测成功;当遇到文件结束符,或者遇到一个无效输入时,istream对象的状态会变为无效,处于无效状态的istream对象会使条件变为假。

int val = 0;

if (std::cin >> val) {
    std::cout << val << std::endl;      // 输入“整数”回车后会执行该语句
}
else {
    std::cerr << "error!" << std::endl; // 输入“字符串”回车后会执行该语句
}

istream与ostream属于不能被拷贝的类型,只能通过引用来传递它们;因为读取和写入的操作会改变流的内容,所以只能是普通引用,而非对常量的引用。

类的成员变量初始化

C++11中类的成员变量初始化的几种不同形式及差异:

// VS2015中
class A
{
private:
    double d = 12.34;
    int b = d;    // warning C4244: “初始化”: 从“double”转换到“int”,可能丢失数据
    int a{d};     // error C2397: 从“double”转换到“int”需要收缩转换
    int c = {d};  // error C2397: 从“double”转换到“int”需要收缩转换
};

decltype类型指示符

decltype((a))的结果永远是引用,decltype(a)只有当a本身是引用时才是引用:

int a = 0;
decltype((a)) b;  // error C2530: “b”: 必须初始化引用
decltype(a) c;


int m = 0;
int& n = m;
decltype(n) v;  // error C2530: “v”: 必须初始化引用

decltype作用于某个函数时,返回函数类型而非指针类型,可以显式地加上*以表明需要返回指针。

数组

数组名的本质就是数组里第一个变量的地址。

int arr[2][3] = {1,2,3,4,5,6};
printf("%p,%p,%p\n", arr, &arr, *arr); // 地址相同
printf("%d,%d,%d\n", sizeof(*arr), sizeof(*&arr), sizeof(**arr)); // 12,24,4
  • 数组当作函数参数时,传递的是指针
    void func(int arr[], int n)
    {
      printf("%d\n", sizeof(arr)); // 4
    }
    void main()
    {
      int arr[10] = {0};
      printf("%d\n", sizeof(arr)); // 40
      func(arr, 10);
    }
    
  • 指针与数组的关系
    // a[i] <==> *(a+i)
    // a[i][j] <==> *(a[i]+j) <==> *(*(a+i)+j)
    // a[i][j][k] <==> *(a[i][j]+k) <==> *(*(a[i]+j)+k) <==> *(*(*(a+i)+j)+k)
    
  • 下面程序的输出结果是(10,20,30)
#include<iostream.h>
void main() 
{
    int n[][3]={10,20,30,40,50,60};
    int (*p)[3];
    p=n;
    cout<<p[0][0]<<","<<*(p[0]+1)<<","<<(*p)[2]<<endl;
}
  • 下列代码的结果是(2,5)
    void main() 
    { 
      int a[5]={1,2,3,4,5}; 
      int *ptr=(int *)(&a+1); 
      printf("%d,%d",*(a+1),*(ptr-1)); 
    }
    

    数组名就是数组0号元素的地址。 a = &a[0] &a 是指向一个有5个整型元素的数组的地址。 a是一维指针,&a相当于是二维指针。 &a+1 就是从a向后跳过一个完整的数组所占用的内存空间。 整型5个元素的数组占用 5sizeof(int)=54=20,所以 &a+1应该从a向后跳20字节。正好指到a[4]的后面。ptr是int *, 减1就是向前跳4个字节,ptr-1正好指向a[4]。

内存分配

  • calloc、malloc

  • 函数malloc不能初始化所分配的内存空间,而函数calloc能。如果由malloc分配的内存空间原来没有被使用过,则其中的每一位可能都是0;反之, 如果这部分内存曾经被分配过,则其中可能遗留有各种各样的数据,也就是说,使用malloc的程序开始时(内存空间还没有被重新分配)能正常进行,但经过一段时间(内存空间还已经被重新分配)可能会出现问题。

  • 函数calloc会将所分配的内存空间中的每一位都初始化为零,也就是说,如果你是为字符类型或整数类型的元素分配内存,那么这些元素将保证会被初始化为0;如果你是为指针类型的元素分配内存,那么这些元素通常会被初始化为空指针;如果你为实型数据分配内存,则这些元素会被初始化为浮点型的零。

// Allocate and zero-initialize array
void* calloc (size_t num, size_t size);
  • C++编译器对变量声明的处理:规划一块内存空间,执行时才真正分配内存空间。

  • 浅拷贝和深拷贝

在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。

深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。

  • 内存分配方式以及它们的区别

1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。

2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集。

3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。

内存申请与一级二级指针

  1. 如果是函数内进行内存申请,很简单,标准用法就可以了:
    void test()
    {
     int* arr;
     // 申请10*4 bytes,即10个单位的int内存单元
     arr = (int*)malloc(sizeof(int) * 10);
    }
    
  2. 使用一级指针实现内存申请,通过函数返回值带出malloc的地址: ```c char* my_malloc(int m) { char* p; p = malloc(m); return p; }

void test() { char* buf = NULL; // 指针如果在函数内没有赋值,注意开始赋值为NULL buf = my_malloc(10); printf(“buff adress is %x\n”, buff); free(buff);
}


3. 使用二级指针实现内存申请,通过指针值传递:
```c
void my_malloc(char** p, int m)
{
    *p = (char*)malloc(m);
}

void test()
{
    char* buf = NULL;
    my_malloc(&buf);
    printf("buffer adress is %x\n", buffer);
    free(buffer);
}

总结:

一级指针和二级指针在做形参时的不同:指针用作形参,改变指针地址则值不能传回,改变指针内容而地址不变则值可以传回。(特殊情况:改变指针地址采用返回值也可以传回地址)

对于一级指针,做形参时传入地址,如果函数只改变该指针内容,OK,该指针可以正常返回,如果函数改变了指针地址,除非返回该指针,否则该指针不能正常返回,函数内对指针的操作将无效。

对于二级指针,做形参时传入地址(注意此时传入的是二级指针的地址),如果改变该二级指针地址(*p),对该指针的操作也将无效,但是改变二级指针的内容(例如p),则该二级指针可以正常返回。 总之,指针使用最关键的是弄清地址和内容,指针做形参时只有改变其内容时才能正常返回。

变量

  • 提升变量的作用域
    extern int x; // 提升x的作用域
    void main()
    {
      printf("%d\n", x);
    }
    int x = 10;
    

字符串

std::string

当把string对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符(+)的两侧的运算对象至少有一个是string。切记:字符串字面值与string是不同的类型。

string s2 = "hello" + ", " + s1; /*  error:不能将字面值直接相加 */
等价于:string s2 = ("hello" + ", ") + s1;

string s3 = s1 + ", " + "world"; /* 正确 */
等价于: string s3 = (s1 + ", ") + "world";
等价于: string tmp = s1 + ", "; 
string s3 = tmp + "world";

无法保证c_str()返回的数组一直有效,如果执行完c_str()函数后想一直都能使用其返回的数组,最好将该数组重新拷贝一份。

std::string s = "abc";
const char* ch = s.c_str();
printf("%s\n", ch);  // abc
s = "123";
printf("%s\n", ch);  // 123

strcpy_s

// ok
char* str;
char* s = "Hello";
int len = std::strlen(s);
str = new char[len + 1];
strcpy_s(str, len + 1, s);


// Expression: (L"Buffer is too small" && 0)
char* str;
char* s = "Hello";
int len = std::strlen(s);
str = new char[len + 1];
strcpy_s(str, 1, s);
// from C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\crt\src\tcscpy_s.inl

/***
*tcscpy_s.inl - general implementation of _tcscpy_s
*
*       Copyright (c) Microsoft Corporation. All rights reserved.
*
*Purpose:
*       This file contains the general algorithm for strcpy_s and its variants.
*
****/

_FUNC_PROLOGUE
errno_t __cdecl _FUNC_NAME(_CHAR *_DEST, size_t _SIZE, const _CHAR *_SRC)
{
    _CHAR *p;
    size_t available;

    /* validation section */
    _VALIDATE_STRING(_DEST, _SIZE);
    _VALIDATE_POINTER_RESET_STRING(_SRC, _DEST, _SIZE);

    p = _DEST;
    available = _SIZE;
    while ((*p++ = *_SRC++) != 0 && --available > 0)
    {
    }

    if (available == 0)
    {
        _RESET_STRING(_DEST, _SIZE);
        _RETURN_BUFFER_TOO_SMALL(_DEST, _SIZE);
    }
    _FILL_STRING(_DEST, _SIZE, _SIZE - available + 1);
    _RETURN_NO_ERROR;
}

STL

STL是建立在泛化之上的

  • 数组泛化为容器,参数化了所包含的对象的类型。
  • 函数泛化为算法,参数化了所用的迭代器的类型。
  • 指针泛化为迭代器,参数化了所指向的对象的类型。

使容器里对象的拷贝操作轻量而正确

拷贝对象是STL的方式,关键如何使容器里对象的拷贝操作轻量而正确。

由于继承的存在,拷贝会导致分割。如果以基类对象建立一个容器,而试图插入派生类对象时,那么当对象(通过基类的拷贝构造函数)拷贝到容器的时候对象的派生部分会被删除。

分割问题暗示了把一个派生类对象插入基类对象的容器几乎总是错的。

一个使拷贝更高效、正确而且对分割问题免疫的简单的方式是建立指针的容器而不是对象的容器。不幸的是,指针的容器有它们自己STL相关的头疼问题。如果想避免这些头疼并且仍要避开效率、正确性和分割问题,智能指针的容器是一个吸引人的选择。

用empty来代替检查size()是否为0

事实上empty的典型实现是一个返回size是否返回0的内联函数。

应该首选empty的构造,而且理由很简单:对于所有的标准容器,empty是一个常数时间的操作,但对于一 些list实现,size花费线性时间。

尽量使用区间成员函数代替它们的单元素兄弟

给定两个vector,v1和v2,使v1的内容和v2的后半部分一样的最简单方式:

v1.assign(v2.begin() + v2.size() / 2, v2.end());

避免手写显示循环,循环也会造成一个效率的损失。

避免手写循环的一个方法,使用copy算法代替。但是copy中的确存在一个循环,效率损失仍然存在。

v1.clear();
copy(v2.begin() + v2.size() / 2, v2.end(), back_inserter(v1));

这个copy的调用可以用一个insert的区间版本代替:

v1.insert(v1.end(), v2.begin() + v2.size() / 2, v2.end());

几乎所有目标区间被插入迭代器指定的copy的使用都可以用调用的区间成员函数的来代替。

  • 区间构造
container::container(InputIterator begin, // 区间的起点
    InputIterator end); // 区间的终点
  • 区间插入
// 所有标准序列容器都提供这种形式的insert
void container::insert(iterator position, // 区间插入的位置
    InputIterator begin, // 插入区间的起点
    InputIterator end); // 插入区间的终点
// 关联容器使用它们的比较函数来决定元素要放在哪里,所以它们了省略position参数
void container::insert(lnputIterator begin, InputIterator end);
  • 区间删除
// 序列容器
iterator container::erase(iterator begin, iterator end);
// 关联容器
void container::erase(iterator begin, iterator end);

区间成员函数更容易写,它们更清楚地表达你的意图,而且它们提供了更高的性能。

使用reserve来避免不必要的重新分配

对于vector和string,只要需要更多空间,就以realloc等价的思想来增长,这个类似与realloc的操作有四个部分:

  1. 分配新的内存块,它有容器目前容量的几倍。在大部分实现中,vector和string的容量每次以2为因数增长。
  2. 把所有元素从容器的旧内存拷贝到它的新内存。
  3. 销毁旧内存中的对象。
  4. 回收旧内存。
  • size():容器中有多少元素。
  • capacity():容器在它已经分配的内存中可以容纳多少元素。
// 在大多数STL实现中,下面一段代码将会导致2~10次重新分配
vector<int> v;
for (int i = 0; i <= 1000; ++i) {
    v.push_back(i);
}

// 不会发生重新分配
vector<int> v;
v.reserve(1000);
for (int i = 0; i <= 1000; ++i) {
    v.push_back(i);
}

调用reserve不改变容器中对象的个数。

std::vector

vector是模板而非类型,vector是类型。

vector中能够容纳绝大多数类型的对象作为其元素,但是因为引用不是对象,所以不存在包含引用的vector。

因为所有标准库容器的迭代器都定义了==和!=,但是它们中的大多数都没有定义<运算符,所以for循环中使用!=而非<进行判断。

size_t是一种机器相关的无符号类型,它被设计的足够大,能够表示内存中任意对象的大小。

clear()不会清空vector的内存

尽管clear()会调用vector中元素的析构函数,但是并不会释放掉元素所占用的内存。这并不难理解,因为在vector为空的时候,我们也可以用reserver()函数来预分配内存。所以vector所占的内存并不会随着元素的释放而释放。如果你想在vector生命周期结束之前及时释放掉vector的内存,请用一个匿名的vector对象来和已有的vector对象v来swap。

STL容器是否线程安全

STL容器不是线程安全的。对于vector,即使写方(生产者)是单线程写入,但是并发读的时候,由于潜在的内存重新申请和对象复制问题,会导致读方(消费者)的迭代器失效。实际表现也就是招致了core dump。另外一种情况,如果是多个写方,并发的push_back(),也会导致core dump。

加锁是一种解决方案,但是加std::mutex互斥锁确实性能较差。对于多读少写的场景可以用读写锁(也叫共享独占锁),来缓解。C++17引入了std::shared_mutex 。

更多的时候,其实可以通过固定vector的大小,避免动态扩容(无push_back)来做到lock-free,即在开始并发读写之前(比如初始化)的时候,给vector设置好大小(resize)。

当有多个写线程的情况下,并发地插入map/unordered_map都会引发core dump。对此,在某些场景下也可以避免加锁:如果全量的key有办法在并发之前就能拿到的,那么就对这个map,提前做一下insert。并发环境中如果只是修改value,而不是插入新key就不会core dump。不过如果你没办法保证多个写线程不会同时修改同一个key的value,那么可能存在value的覆盖,无法保证这点时,还是需要加锁。

性能相关

  • range-based for loop,最好用 auto,否则容易有额外的拷贝
map<string, int> word_count;
for (const auto& kv : word_count) {
  // kv.first, kv.second
}

// C++17
for (const auto& [word, count] : word_count} {
}

如果你想把 auto 的类型写出来,一定不要忘了 key 是 const。

for (const std::pair<const std::string, int>& kv : word_count) {
}

如果你写成了 pair<string, int>,会造成额外的拷贝:

// 低效的写法:
for (const std::pair<std::string, int>& kv : word_count) {
}

左值和右值

当一个对象被用作“右值”的时候,用的是对象的值(内容);当对象被用作“左值”的时候,用的是对象的身份(在内存中的位置)。

有名字的就是左值,反之,不能取地址的、没有名字的就是右值。

含有可变形参的函数

如果函数的实参数量未知,但是全部实参的类型都相同,可以使用initializer_list类型的形参(C++11)。

#include <iostream>
#include <initializer_list>

void show(std::initializer_list<int> il)
{
    for (auto item : il) {
        std::cout << item << std::endl;
    }
}

int main()
{
    show({ 1,2,3,4,5,6 });
    return 0;
}

构造函数

构造函数不能被声明成const的。

static

class A;

class B
{
private:
    static A a_;  // 正确:静态成员可以是不完全类型
};

static关键字有什么作用?

  • 修饰局部变量时,使得该变量在静态存储区分配内存;只能在首次函数调用中进行首次初始化,之后的函数调用不再进行初始化;其生命周期与程序相同,但其作用域为局部作用域,并不能一直被访问;
  • 修饰全局变量时,使得该变量在静态存储区分配内存;在声明该变量的整个文件中都是可见的,而在文件外是不可见的;
  • 修饰函数时,在声明该函数的整个文件中都是可见的,而在文件外是不可见的,从而可以在多人协作时避免同名的函数冲突;
  • 修饰成员变量时,所有的对象都只维持一份拷贝,可以实现不同对象间的数据共享;不需要实例化对象即可访问;不能在类内部初始化,一般在类外部初始化,并且初始化时不加static;
  • 修饰成员函数时,该函数不接受this指针,只能访问类的静态成员;不需要实例化对象即可访问。

std::array

标准库array的大小也是类型的一部分;不能对内置数组类型进行拷贝,但是array并无此限制。

std::array<int, 3> arr = {1, 2, 3};
std::array<int, 3> arr2 = arr;

swap

除array外,交换两个容器内容的操作保证会很快,因为元素本身并未交换,swap只是交换了两个容器的内部数据结构。 除array外,swap不对任何元素进行拷贝、删除或插入操作,因此可以保证在常数时间内完成。

std::vector<int> v1(10, 1);
std::vector<int> v2(15, 2);
swap(v1, v2);

emplace操作

调用emplace_back时,会在容器管理的内存空间中直接创建对象;而调用push_back则会创建一个局部临时对象,并将其压入容器。

class A
{
public:
    A(int x, const std::string& s)
        : x_(x), s_(s)
    {
    }

private:
    int x_;
    std::string s_;
};

int main()
{
    std::vector<A> v;
    v.push_back(A(1, "abc"));
    v.emplace_back(1, "abc");

    return 0;
}

容器下标操作与at成员函数

std::vector<int> v;
std::cout << v[0];     // 运行时错误
std::cout << v.at(0);  // 抛出一个out_of_range异常

resize与reserve

capacity和reserve只适用于vector和string。

reserve并不改变容器中元素的数量,仅影响预先分配多大的内存空间。

resize除了预留内存以外,还会调用容器元素的构造函数,不仅分配了N个对象的内存,还会构造N个对象。

容器的size是指它已经保存的元素的数目;而capacity则是在不分配新的内存空间的前提下它最多可以保存多少元素。

std::vector<int> v = {1, 2, 3};  // 1,2,3
v.resize(6);                     // 1,2,3,0,0,0
v.resize(1);                     // 1


std::vector<int> v = {1, 2, 3};  // 1,2,3; size = 3, capacity = 3
v.reserve(6);                    // 1,2,3; size = 3, capacity = 6
v.reserve(2);                    // 1,2,3; size = 3, capacity = 6

泛型算法

#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>

// 将str中的strsrc替换成strdst
void StrReplace(std::string& str, const std::string& strsrc, const std::string& strdst)
{
    std::string::size_type pos = 0;
    auto srclen = strsrc.size();
    auto dstlen = strdst.size();

    while ((pos = str.find(strsrc, pos)) != std::string::npos)
    {
        str.replace(pos, srclen, strdst);
        pos += dstlen;
    }
}

int main()
{
    std::vector<int> vec = {1, 2, 3};
    // 对vec中的元素求和,和的初始值是0
    int sum = accumulate(vec.cbegin(), vec.cend(), 0);  // 6

    std::vector<std::string> svec = {"hello", "world"};
    // std::string str1 = accumulate(svec.cbegin(), svec.cend(), "$");  // 错误:const char*上没有定义+运算符
    std::string str2 = accumulate(svec.cbegin(), svec.cend(), std::string("$"));  // $helloworld

    std::string str3("hello world, hello cpp!");
    StrReplace(str3, "hello", "hi");

    return 0;
}
  • sort 和 unique算法
// 消除vector中的重复单词
void elimDups(vector<string>& words) 
{
    sort(words.begin(), words.end());
    
    // unique重排输入序列,使得不重复的元素出现在序列的开始部分,返回最后一个不重复元素之后的位置
    auto end_unique = unique(words.begin(), words.end());

    /* 
    标准库算法对迭代器而不是容器进行操作,算法不能(直接)添加或删除元素,
    可以使用vector的erase成员来完成真正的删除操作
    */
    words.erase(end_unique, words.end());
}

关联容器

关联容器对于关键字类型的要求

  • 对于有序容器(map、multimap、set以及multiset),关键字必须定义元素比较的方法。

严格弱序(strict weak ordering): 两个关键字不能同时“<=”对方;如果k1 <= k2且k2 <= k3,那么k1 <= k3; 如果存在两个关键字,任何一个都不“<=”另一个,则两个关键字等价,容器将它们视作相等来处理。

  • 对于无序容器(unordered…),不是使用比较运算符来组织元素,而是使用一个哈希函数和关键字类型的==运算符。

在某些应用中,维护元素的序代价非常高,此时无序容器非常有用。

无序容器在存储上组织为一组桶,每个桶保存零个或多个元素。无序容器使用一个哈希函数将元素映射到桶。

不能直接定义关键字类型为自定义类类型的无序容器,需要提供函数来代替==运算符和哈希值计算函数。

class A
{
public:
    A(int x) : x_(x) {}
    ~A() {}
    int x() const { return x_; }

private:
    int x_;
    std::string s_;
};

int main()
{
    std::unordered_set<A> us;  // error C2338: The C++ Standard doesn't provide a hash for this type.
    return 0;
}
class A
{
public:
    A(int x) : x_(x) {}
    ~A() {}
    int x() const { return x_; }

private:
    int x_;
    std::string s_;
};

size_t myHash(const A& a)
{
    return std::hash<int>()(a.x());
}

bool myEqu(const A& a1, const A& a2)
{
    return a1.x() == a2.x();
}

int main()
{
    std::unordered_set<A, decltype(myHash)*, decltype(myEqu)*> us(10, myHash, myEqu);  // 10: 桶的大小

    A a(123);
    us.insert(a);

    return 0;
}

lower_bound和upper_bound不适用于无序容器。

std::map<int, int> mp;
mp[1] = 123;
mp[4] = 234;
mp[5] = 345;

auto it1 = mp.lower_bound(4);  // 返回一个迭代器, 指向第一个关键字不小于k的元素
auto it2 = mp.upper_bound(4);  // 返回一个迭代器, 指向第一个关键字大于k的元素

int k1 = it1->first;   // 4
int v1 = it1->second;  // 234

int k2 = it2->first;   // 5
int v2 = it2->second;  // 345

// 如果返回相同的迭代器,则给定的关键字不在容器中
if (mp.lower_bound(3) == mp.upper_bound(3))
{
    std::cout << "关键字" << 3 << "不在容器中!" << std::endl;
}
  • std::map遍历过程中删除元素
auto iter = mp.begin();
while (iter != mp.end()) {
    if (iter->second % 2 == 0) {
        mp.erase(iter++);
    }
    else {
        ++iter;
    }
}
  • map[]与at()

如果map中不存在, []将会添加, at会抛出异常。

  • STL中set底层实现方式? 为什么不用hash?

set底层实现方式为RB树(即红黑树)。首先set,不像map那样是key-value对,它的key与value是相同的。关于set有两种说法,第一个是STL中的set,用的是红黑树;第二个是hash_set,底层用得是hash table。红黑树与hash table最大的不同是,红黑树是有序结构,而hash table不是。但不是说set就不能用hash,如果只是判断set中的元素是否存在,那么hash显然更合适,因为set的访问操作时间复杂度是log(N)的,而使用hash底层实现的hash_set是近似O(1)的。然而,set应该更加被强调理解为“集合”,而集合所涉及的操作并、交、差等,即STL提供的如交集set_intersection()、并集set_union()、差集set_difference()和对称差集set_symmetric_difference(),都需要进行大量的比较工作,那么使用底层是有序结构的红黑树就十分恰当了,这也是其相对hash结构的优势所在。

vector和deque的底层实现有什么区别

vector底层是一个数组,动态拓展或动态缩减,是以申请新的内存空间并把数据移动到新内存空间上为代价,所有的元素始终在一个连续物理空间上线性存放。这决定了vector的访问非常高效,同时尾部操作也非常高效。

deque的底层则是若干个数组的集合,单个数组内是连续物理空间,但不同数组间却不连续;deque的动态拓展或动态缩减,是通过新增或者释放物理空间片段来实现的,不发生数据的转移。deque内部维护了所有元素的必要信息,保证能够通过统一的接口直接访问所有的元素,且访问耗时相等,访问者无需关心各个元素是否位于同一个物理空间上。deque的底层设计决定了deque的以下特性:

  • 支持高效的双端增减操作(因为无需移动数据);
  • 在元素数量很大时,总体来说比vector更高效(大量数据的移动很耗时);
  • 不支持“指针+offset”的访问方式(物理空间不连续)
  • 当需要在首尾之外的位置频繁插入/移除元素时,deque比list/forward_list表现更差;
  • 迭代器访问或者引用访问的连续性不如 list 和 forward_list。

迭代器什么时候会失效

可以将迭代器理解成为一个指针,但它又不是我们所谓普通的指针,可以称之为广义指针。

sizeof(std::vector<int>::iterator);  // VS2015中为12

对于vector而言,添加和删除操作可能使容器的部分或者全部迭代器失效。那为什么迭代器会失效呢?vector元素在内存中是顺序存储,试想:如果当前容器中已经存在了10个元素,现在又要添加一个元素到容器中,但是内存中紧跟在这10个元素后面没有一个空闲空间,而vector的元素必须顺序存储一边索引访问,所以我们不能在内存中随便找个地方存储这个元素。于是vector必须重新分配存储空间,用来存放原来的元素以及新添加的元素:存放在旧存储空间的元素被复制到新的存储空间里,接着插入新的元素,最后撤销旧的存储空间。这种情况发生,一定会导致vector容器的所有迭代器都失效。

vector迭代器的几种失效的情况:

  • 当插入(push_back)一个元素后,end操作返回的迭代器肯定失效;
  • 当插入(push_back)一个元素后,capacity返回值与没有插入元素之前相比有改变,则需要重新加载整个容器,此时first和end操作返回的迭代器都会失效;
  • 当进行删除操作(erase,pop_back)后,指向删除点的迭代器全部失效;指向删除点后面的元素的迭代器也将全部失效。

动态内存

如果使用shared_ptr智能指针管理的资源不是new分配的内存,需要传递给它一个删除器(deleter)。unique_ptr管理删除器的方式与shared_ptr不同。

class A
{
public:
    A() { std::cout << "A()" << std::endl; }
    ~A() { std::cout << "~A()" << std::endl; }
};

void myDeleter(A* pa)
{
    std::cout << "void myDeleter(A* pa)" << std::endl;
}

int main()
{
    {
        A a;
        std::shared_ptr<A> spa(&a, myDeleter);
    }
    {
        A a;
        std::unique_ptr<A, decltype(myDeleter)*> spa(&a, myDeleter);
    }
    return 0;
}

与shared_ptr不同,没有类似make_shared的标准库函数返回一个unique_ptr,当定义unique_ptr时,需要将其绑定到一个new返回的指针上。

如果不用另一个智能指针来保存release返回的指针,就需要负责资源的释放。

{
    std::unique_ptr<A> upa(new A);
    std::unique_ptr<A>(upa.release());
}

{
    std::unique_ptr<A> upa(new A);
    A* pa = upa.release();
    delete pa;
}

weak_ptr是一种不控制所指对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。

由于对象可能不存在,因此,不能使用weak_ptr直接访问对象,而必须调用lock()。

auto p = make_shared<int>(123);
weak_ptr<int> wp(p);  // wp弱共享p, p的引用计数未改变

// 若np不为空, 则条件成立
if(shared_ptr<int> np = wp.lock()) {
    cout << *np << endl;
}
  • std::bad_weak_ptr与std::enable_shared_from_this

在异步调用中,存在一个保活机制,异步函数执行的时间点我们是无法确定的,然而异步函数可能会使用到异步调用之前就存在的变量。为了保证该变量在异步函数执期间一直有效,我们可以传递一个指向自身的share_ptr给异步函数,这样在异步函数执行期间share_ptr所管理的对象就不会析构,所使用的变量也会一直有效了(保活)。

struct Bad
{
    std::shared_ptr<Bad> getptr() {
        return std::shared_ptr<Bad>(this);
    }
    ~Bad() { std::cout << "Bad::~Bad() called\n"; }
};

struct Good : public std::enable_shared_from_this<Good> // note: public inheritance
{
    std::shared_ptr<Good> getptr() {
        return shared_from_this();
    }
};

int main()
{
    // Good: the two shared_ptr's share the same object
    std::shared_ptr<Good> gp1 = std::make_shared<Good>();
    std::shared_ptr<Good> gp2 = gp1->getptr();

    std::cout << "gp1.use_count() = " << gp1.use_count() << '\n';  // 2
    std::cout << "gp2.use_count() = " << gp2.use_count() << '\n';  // 2

    // Bad: shared_from_this is called without having std::shared_ptr owning the caller 
    try {
        Good not_so_good;
        std::shared_ptr<Good> gp1 = not_so_good.getptr();
    }
    catch (std::bad_weak_ptr& e) {
        std::cout << "exception: " << e.what() << '\n';
    }

    // Bad, each shared_ptr thinks it's the only owner of the object
    std::shared_ptr<Bad> bp1 = std::make_shared<Bad>();
    std::shared_ptr<Bad> bp2 = bp1->getptr();
    std::cout << "bp1.use_count() = " << bp1.use_count() << '\n';  // 1
    std::cout << "bp2.use_count() = " << bp2.use_count() << '\n';  // 1

    return 0;

    // undefined behavior: double-delete of Bad
}
  • 产生std::bad_weak_ptr异常的原因

shared_from_this()是enable_shared_from_this的成员函数,返回shared_ptr; 注意的是,这个函数仅在shared_ptr的构造函数被调用之后才能使用。 原因是enable_shared_from_this::weak_ptr并不在构造函数中设置,而是在shared_ptr的构造函数中设置。

// 错误示例1:

#include <memory>  
#include <iostream>  
using namespace std;
  
class A: public std::enable_shared_from_this<D>  
{  
public:  
    A()  
    {  
        cout<<"A::A()"<<endl;  
        std::shared_ptr<A> p = shared_from_this();  
    }      
};  
  
int main()  
{  
    std::shared_ptr<A> a = std::make_shared<A>();  
    return 0;      
}  

在D的构造函数中调用shared_from_this(), 此时A的实例本身尚未构造成功,weak_ptr也就尚未设置,所以程序抛出std::bad_weak_ptr异常。

// 错误示例2:

#include <memory>
#include <iostream>  
using namespace std;  
  
class D: public std::enable_shared_from_this<D>  
{  
public:  
    D()  
    {  
        cout<<"D::D()"<<endl;  
    }  
      
    void func()  
    {  
        cout<<"D::func()"<<endl;  
        std::shared_ptr<D> p = shared_from_this();  
    }      
};  
  
int main()  
{  
    D d;  
    d.func();  
    return 0;      
}  

在主函数main中,D的实例是在栈上构造,没有使用std::shared_ptr 的构造方式,所以std::enable_shared_from_this中的weak_ptr所指的函数对象也就没有被赋值。

如果一个函数有多处return,我想在每个return都加一些相同的处理,最好怎么实现?

这里最好的写法就是RAII模式的应用。不过很多时候我们也不必每次新建一个RAII模式的类。可以使用unique_ptr来完成。

shared_ptr修改指向时有时是有开销的

当然我这里要讲的并不是shared_ptr这个对象复制的开销。这个开销很小。shared_ptr一般实现中里面就是存了两个指针成员,shared_ptr对象的size也就是普通指针的2倍而已。复制开销也不大。 我这里说的是,当一个已经存在的shared_ptr修改指向的时候:

shared_ptr<T> p4 = p2; 
... 
p4 = p1; 

如果p4是最后一个持有旧对象的shared_ptr的话,那么当p4 = p1的时候,不止是触发shared_ptr成员变量复制操作。还会触发旧对象的析构操作!这不难理解,我们都知道shared_ptr在所管理的数据对应的引用计数清零的时候,会触发析构操作。只是很多时候我们都把智能指针当普通指针用的时候,常常会忽略这一操作的潜在开销。

引用计数具体是怎么实现的?怎么做到多个shared_ptr之间的计数能共享,同步更新的呢?

shared_ptr中除了有一个指针,指向所管理数据的地址。还有一个指针指向一个控制块的地址,里面存放了所管理数据的数量(常说的引用计数)、weak_ptr的数量、删除器、分配器等。

也就是说对于引用计数这一变量的存储,是在堆上的,多个shared_ptr的对象都指向同一个堆地址。在多线程环境下,管理同一个数据的shared_ptr在进行计数的增加或减少的时候是线程安全的吗?

答案是肯定的,这一操作是原子操作。

To satisfy thread safety requirements, the reference counters are typically incremented using an equivalent of std::atomic::fetch_add with std::memory_order_relaxed (decrementing requires stronger ordering to safely destroy the control block).

拷贝控制

拷贝构造函数的第一个参数必须是一个引用类型。如果其参数不是引用类型,则调用永远也不会成功,为了调用拷贝构造函数,必须拷贝它的实参,但为了拷贝实参,又需要拷贝构造函数,如此无限循环。

当编写赋值运算符时,一个好的模式是:先将右侧运算对象拷贝到一个局部临时对象中;当拷贝完成后,销毁左侧运算对象的现有成员就是安全的了;最后再将数据从临时对象拷贝到左侧运算对象的成员中。

class A
{
public:
    A(const char* s, int x) : str_(new std::string(s)), x_(x) {}
    ~A() { delete str_; }

    A(const A& a) : str_(new std::string(*(a.str_))), x_(a.x_) { }

    A& operator=(const A& a)
    {
        auto news = new std::string(*(a.str_));
        delete str_;
        str_ = news;
        x_ = a.x_;
        return *this;
    }

private:
    std::string* str_;
    int x_;
};

int main()
{
    A a1("a1", 1);
    A a2("a2", 2);
    a2 = a1;

    return 0;
}
  • 对象移动

IO类和unique_ptr类可以移动但不能拷贝。

右值引用和move语义

右值引用:必须绑定到右值的引用,通过&&而不是&来获得右值引用。

右值引用有一个重要的性质:只能绑定到一个将要销毁的对象。左值持久,右值短暂。

#include <utility>

int main()
{
    int a = 0;
    //int&& b = a;             // 错误:无法将左值绑定到右值引用
    int&& c = 0;               // 正确:字面常量是右值
    int&& d = std::move(a);    // 正确:显示地将一个左值转换为对于地右值引用类型

    return 0;
}

由于一个移后源对象具有不确定的状态,当调用move时,必须绝对确认移后源对象没有其他用户。

引入右值引用的主要目的是提高程序运行的效率。当类持有其它资源时,例如动态分配的内存、指向其他数据的指针等,拷贝构造函数中需要以深拷贝(而非浅拷贝)的方式复制对象的所有数据。深拷贝往往非常耗时,合理使用右值引用可以避免没有必要的深拷贝操作。

#include <iostream>
#include <string>
#include <cstring>

class ZString
{
public:
    ZString() : str_(new char[1]) {
        str_[0] = 0;
    }

    ZString(const char* s) {
        printf("ZString(const char* s)\n");
        int len = strlen(s) + 1;
        str_ = new char[len];
        strcpy_s(str_, len, s);
    }

    ZString(const ZString& s) {
        printf("ZString(const ZString& s)\n");
        int len = strlen(s.str_) + 1;
        str_ = new char[len];
        strcpy_s(str_, len, s.str_);
    }

    ZString& operator=(const ZString& s) {
        printf("ZString& operator=(const ZString& s)\n");
        if (str_ != s.str_)
        {
            delete[] str_;

            int len = strlen(s.str_) + 1;
            str_ = new char[len];
            strcpy_s(str_, len, s.str_);
        }
        return *this;
    }

    ZString(ZString&& s) : str_(s.str_) {
        printf("ZString(ZString&& s)\n");

        s.str_ = new char[1];
        s.str_[0] = 0;
    }

    ZString& operator=(ZString&& s) {
        printf("ZString& operator=(ZString&& s)\n");
        if (str_ != s.str_)
        {
            str_ = s.str_;
            s.str_ = new char[1];
            s.str_[0] = 0;
        }
        return *this;
    }

    ~ZString() {
        printf("~ZString()\n");
        delete[] str_;
    }

private:
    char* str_;
};

template <class T>
void Swap(T& a, T& b) {
    T tmp = a;
    a = b;
    b = tmp;
}

template <class T>
void MoveSwap(T& a, T& b) {
    T tmp = std::move(a);
    a = std::move(b);
    b = std::move(tmp);
}

int main()
{
    ZString s;

    // 如果没有定义移动赋值运算符, 则会调用拷贝赋值运算符, 产生深拷贝
    s = ZString("hello");
    
    ZString s1 = "s1", s2 = "s2";
    MoveSwap(s1, s2);

    return 0;
}

不借助swap函数,自己实现一个函数来交换两个std::string类型的对象的值。

int main()
{
    std::string str1 = "hello";
    std::string str2 = "world";
    zswap(str1, str2);
    return 0;
}

void zswap(std::string& a, std::string&b)
{
    // a = "hello", b = "world"
    std::string tmp(std::move(a));
    // a = "", b = "world", tmp = "hello"
    a = std::move(b);
    // a = "world", b = "", tmp = "hello"
    b = std::move(tmp);
    // a = "world", b = "hello", tmp = ""
}

重载运算

为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用,后置运算符应该返回值而非引用。

区分前置和后置运算符:后置版本接受一个额外的(不被使用)int类型的形参。

class A
{
public:
    A(int step) : step_(step), position_(0) { }
    ~A() { }

    // 前置++
    A& operator++()
    {
        position_ = position_ + step_;
        return *this;
    }

    // 前置--
    A& operator--()
    {
        position_ = position_ - step_;
        return *this;
    }

    // 后置++
    A operator++(int)
    {
        A a = *this;
        position_ = position_ + step_;
        return a;
    }

    // 后置--
    A operator--(int)
    {
        A a = *this;
        position_ = position_ - step_;
        return a;
    }

private:
    int position_;
    int step_;
};

int main()
{
    A a(5);
    A b = a;

    b = ++a;
    b = --a;

    b = a++;
    b = a--;

    return 0;
}

可调用对象与function

int add(int i, int j) { return i + j; }

auto mod = [](int i, int j) { return i % j; };

struct divide {
    int operator() (int i, int j) {
        return i / j;
    }
};

int main()
{
    std::map<std::string, int(*)(int, int)> ops;
    ops["+"] = add;

    // 但是不能将divide存入ops
    //ops["/"] = divide;

    std::map<std::string, std::function<int(int, int)>> newops;
    newops["+"] = add;
    newops["-"] = std::minus<int>();
    newops["%"] = mod;
    newops["*"] = [](int i, int j) { return i * j; };
    newops["/"] = divide();

    int ret1 = newops["+"](10, 5);
    int ret2 = newops["-"](10, 5);
    int ret3 = newops["*"](10, 5);
    int ret4 = newops["/"](10, 5);
    int ret5 = newops["%"](10, 5);

    return 0;
}
  • 函数指针
double chu(int a, int b) {
    return a / b;
}

void main() {
    int x = 100, y = 20;
    // 创建一个函数指针数组,每一个元素都是一个函数指针
    double (*p[4])(int a, int b) = {jia, jian, cheng, chu};
    for (int i=0; i<4; ++i) {
        printf("%0.1f\n", p[i](x, y));
    }
}

基类和派生类

尽管在派生类对象中含有从基类继承而来的成员,但是派生类并不能直接初始化这些成员。派生类必须使用基类的构造函数来初始化它的基类的部分。每个类控制它自己的成员初始化过程。

class A
{
public:
    A(int x) : x_(x)  {}
    virtual ~A() {}

    int x_;
};

class B : public A
{
public:
    B(int x, int y) : A(x), y_(y) {}
    ~B() {}

    int y_;
};

如果想将某个类用作基类,则该类必须已经定义而非仅仅声明。

  • cpp类体系中,不能被派生类继承的有(A、D)
A. 构造函数
B. 静态成员函数
C. 非静态成员函数
D. 赋值操作函数

“赋值运算符重载函数”不是不能被派生类继承,而是被派生类的默认”赋值运算符重载函数”给覆盖了。 这就是 C++ 赋值运算符重载函数不能被派生类继承的真实原因!

  • 隐藏与覆盖的区别

C++ 的派生类如果要覆盖一个继承到的成员函数, 在基类中需要将该函数声明为virtual。 因为如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字,此时,基类的函数被隐藏,注意,是隐藏而不是覆盖。覆盖的基本特征之一就是 基类函数必须有virtual关键字。 如果子类要重写父类方法,需要将父类该方法声明为virtual,实现RTTI。当然你可以不这样干,结果就是静态绑定。补充一点,重写就叫覆盖。如果没有virtual就是隐藏。C++ 11新标准中我们可以使用override关键字来说明派生类中的虚函数。(如果我们使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错。)

覆盖:在派生类中覆盖基类中的同名函数,要求两个函数的参数个数、参数类型、返回类型都相同,且基类函数必须是虚函数。

隐藏:派生类中的函数屏蔽了基类中的同名函数,2个函数参数相同,但基类函数不是虚函数(和覆盖的区别在于基类函数是否是虚函数)。2个函数参数不同,无论基类函数是否是虚函数,基类函数都会被屏蔽(和重载的区别在于两个函数不在同一类中)。

虚函数 & 多态

  • 多态是面向对象非常重要的一个特性;
  • 同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果;
  • 在运行时,可以识别出真正的对象类型,调用对应子类中的函数。

  • 回避虚函数的机制
class A
{
public:
    A() {}
    virtual ~A() {}
    virtual void func() {
        std::cout << "A::func()" << std::endl;
    }
};

class B : public A
{
public:
    B(){}
    ~B() {}

    void func() override {
        std::cout << "B::func()" << std::endl;
    }
};

int main()
{
    B b;
    A* pBase = &b;
    // 强行调用基类中定义的函数版本而不管pBase的动态类型是什么
    pBase->A::func();

    return 0;
}
  • 空类、含有虚函数的类占用空间大小
class A {};
class B {
    virtual void f() {}
};

int main()
{
    std::cout << sizeof(A) << std::endl;  // 1
    std::cout << sizeof(B) << std::endl;  // 4

    return 0;
}

类的实例化是在内存中分配一块地址,每个实例都有独一无二的内存地址。空类也会实例化,为保证空类实例化后的独一无二性,编译器会给空类隐含的添加一个字节。所以,空类的sizeof不是0,至于占用多少内存,由编译器决定,通常为1。

C++的编译器一旦发现一个类型中有虚拟函数,就会为该类型生成虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针。在32位的机器上,一个指针占4个字节的空间,因此sizeof是4。

模板与泛型编程

  • 非类型模板参数

除了定义类型参数,还可以在模板中定义非类型参数。一个非类型参数表示一个值而非一个类型。

template <unsigned N, unsigned M>
// 数组不能拷贝,将参数定义为数组的引用
int complare(const char (&p1)[N], const char (&p2)[M])
{
    return strcmp(p1, p2);
}

int main()
{
    int x = complare("hello", "world");
    return 0;
}
  • 模板编译

当编译器遇到一个模板定义时,它并不生成代码。只有当我们实例化出模板的一个特定版本时,编译器才会生成代码。函数模板和类模板成员函数的定义通常放在头文件中。

函数模板

template <typename T>
inline T const& max(const T& a, const T& b) {
    return a < b ? b : a;
}

// 调用时在max前面加上'::', 以便确保被调用的是在全局命名空间中定义的max(), 而不是std::max()。
int maxVal = ::max(1, 2);

一般而言,templates不会被编译成”能够处理任意类型”的单一实体(entity),而是被编译为多个个别实体,每一个处理某一特定类型。

以具体类型替代template parameters的过程称为“实例化(instantiation)”。

实际上,templates会被编译2次:

(1) 不实例化,只是对template程序代码进行语法检查以发现诸如“缺少分号”等等的语法错误。

(2) 实例化,编译器检查template程序代码中的所有调用是否合法。

函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必须由程序员在程序中显式地指定。 即函数模板允许隐式调用和显式调用,而类模板只能显式调用。

max(1, 2);    // ok
max(1, 2.2);  // error
max(static_cast<double>(1), 2.2); // ok
max<double>(1, 2.2); // ok
  • 重载

function templates也可以被重载,non-template function可以和同名的function template共存,当其它要素都相等时,重载解析机制会优先选择non-template function。

类模板

与函数模板的不同之处是,编译器不能为类模板推断模板参数类型。为了使用类模板,必须在模板名后的尖括号中提供额外信息。

在使用以.h.cpp分离实现模板类时,不能像使用普通类一样只简单的包涵.h头文件,应该在使用模板类的cpp文件中引入模板类相应的cpp文件。

#include <vector>
#include <stdexcept>

template <typename T>
class Stack
{
private:
    std::vector<T> elems;

public:
    void push(const T&);
    void pop();
    T top() const;
    bool empty() const {
        return elems.empty();
    }
};


template <typename T>
void Stack<T>::push(const T& elem) {
    elems.push_back(elem);
}

template <typename T>
void Stack<T>::pop() {
    if (elems.empty()) {
        throw std::out_of_range("empty stack!");
    }
    elems.pop_back();
}

template <typename T>
T Stack<T>::top() const {
    if (elems.empty()) {
        throw std::out_of_range("empty stack!");
    }
    return elems.back();
}

// 使用
Stack<int> stack;
bool empty = stack.empty();
  • 类模板与模板类

类模板的重点是模板,表示的是一个模板,专门用于产生类的模子。

template <typename T>
class Vector
{
  ...
};

使用这个Vector模板就可以产生很多的class(类),如Vector、Vector等。

模板类的重点是类,表示的是由一个模板生成而来的类,上面的Vector、Vector全是模板类。

  • Class Templates的特化(Specializations)

模板为什么要特化,因为编译器认为,对于特定的类型,如果你能对某一功能更好的实现,那么就该听你的。

想要特化某个class template,必须以template<>开头声明此class,后面跟着希望的特化结果。

template<>
class Stack<std::string>
{
    // ...
};

对特化体而言,每个成员函数都必须像常规的成员函数那样定义,每一个出现T的地方都必须更换为特化类型。

template <typename T>
class Stack
{
private:
    std::vector<T> elems;
public:
    void push(const T&) {
        // ...
    }
};

// 特化
template<>
class Stack<std::string>
{
private:
    std::vector<std::string> elems;
public:
    void push(const std::string& elem);
};

void Stack<std::string>::push(const std::string& elem) {
    elems.push_back(elem);
}
  • 偏特化

class templates可以被偏特化(partial specialized,或称为部分特化、局部特化)。

template <typename T1, typename T2>
class MyClass
{

};

// 一下多种形式的偏特化都是合理的

// 偏特化: 两个template parameter相同
template <typename T>
class MyClass<T, T>
{

};

// 偏特化:第二个类型为int
template <typename T>
class MyClass<T, int>
{

};

// 偏特化:两个template parameter均为指针类型
template <typename T1, typename T2>
class MyClass<T1*, T2*>
{

};
  • 预设模板自变量(Default Template Arguments)
#include <vector>
#include <stdexcept>
#include <deque>

template <typename T, typename CONT = std::vector<T> >
class Stack
{
private:
    CONT elems;

public:
    void push(const T&);
    void pop();
    T top() const;
    bool empty() const {
        return elems.empty();
    }
};

template <typename T, typename CONT>
void Stack<T, CONT>::push(const T& elem) {
    elems.push_back(elem);
}

template <typename T, typename CONT>
void Stack<T, CONT>::pop() {
    if (elems.empty()) {
        throw std::out_of_range("empty stack!");
    }
    elems.pop_back();
}

template <typename T, typename CONT>
T Stack<T, CONT>::top() const {
    if (elems.empty()) {
        throw std::out_of_range("empty stack!");
    }
    return elems.back();
}

// 使用
int main()
{
    Stack<int> stack1;
    bool empty = stack1.empty();

    Stack<double, std::vector<double> > stack2;
    empty = stack2.empty();

    Stack<std::string, std::deque<std::string> > stack3;
    empty = stack3.empty();

    return 0;
}

tuple类型

当我们希望将一些数据组合成单一对象,但又不想麻烦地定义一个新数据结构来表示这些数据时,tuple是非常有用的。

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

int main()
{
    std::tuple<int, std::string, std::vector<double>> tp1(1, "a", {1.0, 2.0});

    int v1 = std::get<0>(tp1);
    std::string v2 = std::get<1>(tp1);
    std::vector<double> v3 = std::get<2>(tp1);

    typedef decltype(tp1) tp1Type;
    size_t sz = std::tuple_size<tp1Type>::value;  // 3

    return 0;
}

bitset

#include <bitset>

int main()
{
    // 0x123: 000100100011
    std::bitset<16> b1(0x123);  // {1,1,0,0,0,1,0,0,1,0,0,0,0,0,0,0}

    std::bitset<8> b2("110");   // {0,1,1,0,0,0,0,0}
    b2.flip();                  // {1,0,0,1,1,1,1,1}

    return 0;
}

宏定义

宏定义不受块作用域的限制。

  • #也就是宏给一个标识符自动加上””
#define go(x) system(#x)  // 传递的参数,x自动加上"", 让其变成一个字符串
例如:
int var = 100;   // 要求输出格式 "var = 100"
#define printNum(x) printf("%s = %d", #x, x)
printNum(var);  // var = 100
  • ##连接符, 把2个语言符号(Token)组合成单个语言符号
#define l(i) x##i

int l(1) = 1;
int l(2) = 2;
int l(3) = 3;
printf("x1 = %d\n", x1);  // x1 = 1
printf("x2 = %d\n", x2);  // x2 = 2
printf("x3 = %d\n", x3);  // x3 = 3
  • __cplusplus
#ifdef __cplusplus
extern "C" {
#endif

// ...

#ifdef __cplusplus
}
#endif

这种类型的头文件既可以被#include到C文件中进行编译,也可以被#include到C++文件中进行编译。由于 extern “C”可以抑制C++对函数名、变量名等符号(symbol)进行名称重整(name mangling),因此编译出的C目标文件 和C++目标文件中的变量、函数名称等符号都是相同的(否则不同),链接器可以可靠地对两种类型的目标文件进行链接。 这种做法成为了C与C++混用头文件的典型做法。

在C++03标准中,__cplusplus的值被预定义为199711L,而在C++11标准中,__cplusplus的值被预定义为201103L。

#if __cplusplus < 201103L
    #error "should use C++11 implementation"
#endif
  • 判断一段程序是由C编译程序还是由C++编译程序编译的?
#ifdef __cplusplus
    cout << "c++";
#else
    cout << "c";
#endif

#define和const有什么区别?

  • 编译器处理方式不同:#define宏是在预处理阶段展开,不能对宏定义进行调试,而const常量是在编译阶段使用;
  • 类型和安全检查不同:#define宏没有类型,不做任何类型检查,仅仅是代码展开,可能产生边际效应等错误,而const常量有具体类型,在编译阶段会执行类型检查;
  • 存储方式不同:#define宏仅仅是代码展开,在多个地方进行字符串替换,不会分配内存,存储于程序的代码段中,而const常量会分配内存,但只维持一份拷贝,存储于程序的数据段中;
  • 定义域不同:#define宏不受定义域限制,而const常量只在定义域内有效。

内联函数源文件(.inl)

.inl文件是内联函数的源文件,内联函数通常在.h头文件中实现,但是当.h头文件中内联函数过多的情况下,我们想使头文件看起来简洁点,能不能像普通函数那样将内联函数声明和函数定义放在头文件和实现文件中呢?当然答案是肯定的,具体做法将是:将内联函数的具体实现放在inl文件中,然后在头文件末尾使用#include引入该inl文件。 对于比较大的工程来说,出于管理方面的考虑,模板函数、模板类的声明一般放在一个或少数几个头文件中,然后将其定义部分放在inl文件中,这样可以让工程结构清晰、明了。

在Google的C++代码编程规范中也说到了inl文件。 例子:

  • Vec3.h
#ifndef MATH_VEC3_H
#define MATH_VEC3_H
class Vec3 {
  public:
    inline void setZero();
};
#include "Vec3.inl"  // 头文件末尾包含.inl文件
#endif  // MATH_VEC3_H
  • Vec3.inl
#include "Vec3.h"  // 包含.h文件
inline void Vec3::setZero() {
    x = y = z = 0.0f;
}
  • Vec3.cpp
#include "Vec3.h"
// ...

内联函数

  • 有什么作用?

减少函数调用带来的开销。

  • 开销指什么?

C++语言程序内存分为常量区、代码区、静态全局区、栈区、堆区。函数代码段被放在所谓的代码区,局部变量与函数参数被放在栈区。函数调用就发生在栈区里面,每次调用的时候会把当前函数的相关内容压入到栈里面处理寄存器相关的数据信息。然后,调用地址指向我们要执行的函数位置,开始处理函数内部的指令进行计算,当函数执行结束后,要弹出相关数据,处理栈内数据以及寄存器数据。这个过程也就是所谓的“函数调用开销”。

  • 消除函数调用的直接好处?

它消除了函数调用过程中所需的各种指令:包括在堆栈或寄存器中放置参数,调用函数指令,返回函数过程,获取返回值,从堆栈中删除参数并恢复寄存器等。由于不需要寄存器来传递参数,因此减少了寄存器溢出的概率。当使用引用调用(或通过地址调用或通过共享调用)时,它消除了必须传递引用然后取消引用它们。

  • 有什么缺点?

使用不当的话就会造成代码膨胀(也就是生成的可执行程序会变大),影响cache对数据的命中,如果你设计了一个函数库,调用你的内联函数还会造成客户代码的重新编译。一般高速缓存里面会分为指令缓存(instruction cache)以及数据缓存(data cache),inline的使用不当对二者都可能造成影响。首先,过多的内联代码会使原来本可以存储到ICache的指令分散,导致指令缓存的命中降低,从内存取数据会严重影响效率。其次,inline会导致代码膨胀,增加可执行程序(动态库、静态库)体积,造成额外的换页行为,进而可能会导致数据缓存的命中率降低。如递归函数的内联可能造成代码的无限inline循环。所以编译器在这些特殊情况下会拒绝内联。其实内联inline只是建议性的关键字,编译器并不一定会听你的。

为什么一个类的空指针可以访问类的成员函数?

class A
{
public:
    void fun() {
        cout << "fun()" << endl;
    }

    virtual void vfun() {
        cout << "virtual fun()" << endl;
    }
};

void mytest()
{
    A* pa = NULL;
    pa->fun();  // 调用成功
    pa->vfun(); // 程序崩溃,报错:引发一场,读取访问权限冲突
}
// 为什么调用fun()可以成功,但是调用虚函数vfun()却不可以呢?
  • 对于函数fun(),因为其是一个no-vritual函数,它是静态绑定的,编译器会根据对象的静态类型来选择函数,pa的静态类型是A*,那么编译器在处理pa->fun()的时候会将它指向A::fun();pa的首地址为NULL,虽然编译器会给该函数传递this指针,this指向pa的首地址,但是由于该函数中没有通过this指针来访问类的成员变量,即没有对this解引用,因此该函数可以正常调用。

  • 对于函数vfun(),因为其是一个virtual函数,它是动态绑定的,绑定的是对象的动态类型,主要是靠虚表(V-Table)来实现的,该表中存放类的虚函数的地址,通过对象调用该虚函数时,会通过虚表查找真正要调用的函数的入口地址,如果对象pa为NULL,则无法找到虚函数表,此时会报错。

类型转换

static_cast

用法:static_cast < type-id > ( expression )

该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:

  • 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。 进行上行转换(把派生类的指针或引用转换成基类表示)是安全的; 进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。
  • 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
  • 把空指针转换成目标类型的空指针。
  • 把任何类型的表达式转换成void类型。

dynamic_cast

dynamic_cast主要用于类层次结构中父类和子类之间指针和引用的转换,由于具有运行时类型检查,因此可以保证下行转换的安全性,何为安全性?即转换成功就返回转换后的正确类型指针,如果转换失败,则返回NULL,之所以说static_cast在下行转换时不安全,是因为即使转换失败,它也不返回NULL。

class Base { virtual void foo() {} };
class Derived : public Base {};

Base* ptr = new Derived;
Derived* dptr = dynamic_cast<Derived*>(ptr);
if (dptr != nullptr) {
    // 转换成功
} else {
    // 转换失败
}

const_cast

const_cast一般用于强制消除对象的常量性。它是唯一能做到这一点的C++风格的强制转型。这个转换能剥离一个对象的const属性,也就是说允许你对常量进行修改。

class C {};
const C *a = new C;
C *b = const_cast<C *>(a);

reinterpret_cast

reinterpret_cast运算符是用来处理无关类型之间的转换;它会产生一个新的值,这个值会有与原始参数(expressoin)有完全相同的比特位。

C++不是类型安全的,因为两个不同类型的指针之间可以强制转换(用reinterpret_cast)。

C++ 11

delete关键字

有些时候我们希望限制默认函数的生成。典型的是禁止使用拷贝构造函数,以往的做法是将拷贝构造函数声明为private的,并不提供实现,这样当拷贝构造对象时编译不能通过。

class AsyncLogging {
private:
  // declare but not define, prevent compiler-synthesized functions
  AsyncLogging(const AsyncLogging&);  // ptr_container
  void operator=(const AsyncLogging&);  // ptr_container
}

C++11则使用delete关键字显式指示编译器不生成函数的默认版本。

class ThreadPool {
public:
    explicit ThreadPool(int num_threads);
    ~ThreadPool();
    ThreadPool(const ThreadPool&) = delete;
    ThreadPool& operator=(const ThreadPool&) = delete;
}

static_assert

编译的时候就可以检测出

static_assert(1 < 0, "hello...");

编译结果:
1>c:\source.cpp(11): error C2338: hello...
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========

using定义别名

template <typename T>
using Tlist = std::list<T>;
using Tlist = std::list<char>;
using df = void(*)(); //等价于typedef void(*df)()

enum class

enum 与 enum class对比

enum OldEnum{ OLD };
enum class NewEnum{ NEW };

void main() {
    int ok1 = OldEnum::OLD;
    int ok2 = OLD;
 
    // int no1 = NEW;  // error C2065: 'NEW' : undeclared identifier
    NewEnum ok3 = NewEnum::NEW;
 
    // int no2 = NewEnum::NEW;  // error C2440: 'initializing' : cannot convert from 'NewEnum' to 'int'
    int ok4 = static_cast<int>(NewEnum::NEW);
}

谓词、lambda表达式

谓词

谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。

一元谓词:只接受单一参数;

二元谓词:有两个参数。

bool isShorter(const string& str1, const string& str2) {
    return str1.size() < str2.size();
}
// 按长度由短至长排序
sort(words.begin(), words.end(), isShorter);

lambda表达式

[capture list] (parameter list) -> return type {function body}

capture list(捕获列表)是一个lambda所在函数中定义的局部变量的列表(通常为空)

sort(words.begin(), words.end(), 
    [](const string& str1, const string& str2) {
        return str1.size() > str2.size(); }
);
// 使用捕获列表
string::size_type sz = 3;

/* 
find_if返回一个迭代器, 指向第一个长度不小于给定参数sz的元素, 
如果这样的元素不存在,则返回words.end()的一个拷贝 
*/
auto wc = find_if(words.begin(), words.end(),
    [sz](const string& str) {
        return str.size() >= sz;
    }
);

if (wc == words.end()) {
    cout << "Not find!" << endl; 
} else {
    cout << *wc << endl; 
}
  • for_each算法
// 打印出words中的所有单词
for_each(words.begin(), words.end(), 
        [](const string& s) { cout << s << " "; }
);  
  • lambda引用捕获

当以引用方式捕获一个变量时,必须保证在lambda执行时变量是存在的。

/*
由于不能拷贝ostream对象, 因此捕获一个os对象的唯一方法就是捕获其引用
(或指向os的指针)
*/
void ShowMembers(vector<string>& words, ostream &os = cout, char c = '|') {
    for_each(words.begin(), words.end(), 
        [&os, c](const string& s) {os << s << c;});
}
  • 显示捕获和隐式捕获

[]、[names]、

[&]、[=]、

[&, identifier_list]、[=, identifier_lsit]

  • 指定lambda的返回类型
// 第三个迭代器表示目的位置
transform(vi.begin(), vi.end(), vi.begin(),
    [](int i) -> int { if (i < 0) return -i; else return i;});

其他

  • 若类A中没有任何成员变量与成员函数, sizeof(A)的值是多少?

肯定不是零。举个反例,如果是零的话,声明一个class A[10]对象数组,而每一个对象占用的空间是零,这时就没办法区分A[0],A[1],…了。

  • volatile关键字

使用volatile关键字可以阻止编译器过度优化: 阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回;阻止编译器调整操作volatile变量的指令顺序。

  • 结构与联合的区别

(1) 结构和联合都是由多个不同的数据类型成员组成,但在任何同一时刻, 联合中只存放了一个被选中的成员 (所有成员共用一块地址空间), 而结构的所有成员都存在 (不同成员的存放地址不同)。

(2) 对于联合的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了,而对于结构的不同成员赋值是互不影响的。

  • unresolved external symbol是什么错误?原因是什么?

不确定的外部“符号”,产生这个错误的原因:如果链接程序不能在所有的库和目标文件内找到所引用的函数、变量或标签,将产生此错误消息。一般来说,发生错误的原因有两个:一是所引用的函数、变量不存在、拼写不正确或者使用错误;其次可能使用了不同版本的链接库。

  • 消除引入第三方头文件产生的编译警告

无法修改的库头文件可能包含引起警告(可能是良性的)的构造。如果这样,可以用自己的包含原头文件的版本将此文件包装起来,并有选择地为该作用域关闭烦人的警告,然后在整个项目的其他地方包含此包装文件。

例如:myprj/my_lambda.h - 包装了boost的lambda.hpp,Boost.Lambda会产生一些已知无害的编译警告。

// myprj/my_lambda.h

#pragma warning(push)
  #pragma warning(disable:4512)
  #pragma warning(disable:4180)
  #include <boost/lambda/lambda.hpp>
#pragma warning(pop)  // 恢复最初的警告级别
  • 柔性数组

开发C代码时,经常见到如下类型的结构体定义:

typedef struct list_t{
    struct list_t *next;
    struct list_t *prev;
    char data[0];
}list_t;

最后一行char data[0];的作用是? (AB)

A.方便管理内存缓冲区 B.减少内存碎片化 C.标识结构体结束 D.没有作用

char data[0]柔性数组,它只能放在结构体末尾,是申明一个长度为0的数组,就可以使得这个结构体是可变长的。对于编译器来说,此时长度为0的数组并不占用空间,因为数组名本身不占空间,它只是一个偏移量, 数组名这个符号本身代 表了一个不可修改的地址常量 (注意:数组名永远都不会是指针! ),但对于这个数组的大小,我们可以进行动态分配 请仔细理解后半部分,对于编译器而言,数组名仅仅是一个符号,它不会占用任何空间,它在结构体中,只是代表了一个偏移量,代表一个不可修改的地址常量! 对于0长数组的这个特点,很容易构造出变成结构体,如缓冲区,数据包等等: 注意:构造缓冲区就是方便管理内存缓冲区,减少内存碎片化,它的作用不是标志结构体结束,而是扩展, 柔性数组是C99的扩展,简而言之就是一个在struct结构里的标识占位符(不占结构struct的空间)。

  • const char *p和char * const p的区别

如果const位于星号的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量; 如果const位于星号的右侧,const就是修饰指针本身,即指针本身是常量。

  • strcpy()和memcpy()的区别

strcpy()和memcpy()都可以用来拷贝字符串,strcpy()拷贝以’\0’结束,但memcpy()必须指定拷贝的长度。

  • 指针与数组

访问指针数组需要二级指针。

指针数组元素的作用相当于二维数组的行名,但指针数组中元素是指针变量,二维数组的行名是地址常量。

二维数组,地址不可以变化,数据可以变化; 指针数组,指向的数据不可以变化,地址可以变化。

二维数组用于字符串顺序固定,同时字符串需要修改的情况。 指针数组用于字符串不需要修改,顺序可以调整的情况。

常用代码

  • 填充字符串(std::setw、std::setfill)
#include <iomanip>

std::ostringstream oss("");

oss.str("");
oss << std::setw(6) << std::setfill('*') << "[END]";
cout << oss.str() << endl;  // *[END]

oss.str("");
oss << std::setw(6) << std::setfill('*') << "[]";
cout << oss.str() << endl;  // ****[]
  • 获取本地文件字节数的方式
// Windows VS2015
#include <sys/stat.h>

off_t GetLocalFileSize(const std::string& filename)
{
    struct stat fileStateBuf;
    memset(&fileStateBuf, 0, sizeof(fileStateBuf));
    stat(filename.c_str(), &fileStateBuf);
    
    off_t st_size = fileStateBuf.st_size;
    std::cout << "filesize = " << st_size << std::endl;
    return st_size;
}
  • C++把科学计数法转成正常数
long a = 9.50413e+006;
std::cout << std::scientific << a << std::endl; // 9504130
std::cout << std::fixed << a << std::endl;      // 9504130
  • 取出十六进制数的二进制的每一位
// 从左往右输出
// 输出: 0 0 0 0 1 0 1 1
int n = 11;  // 0000 1011
int y = 0x80;
for (int i = 0; i < 8; ++i) {
    int x = (n & y) == y ? 1 : 0;
    cout << x << " ";
    y >>= 1;
}

// 从右往左输出
// 输出: 1 1 0 1 0 0 0 0
int n = 0x0B;  // 0000 1011
for (int i = 0; i < 8; ++i) {
    cout << (n & 1) << " ";
    n >>= 1;
}
  • char转成std::string
std::string s1 = std::string(1, 'a');  // "a"
std::string s2 = std::string(2, 'a');  // "aa"
std::string s3 = std::string(3, 'a');  // "aaa"
  • std::string assign()
std::string str1("abcde");
std::string str2;

// 直接用另一个字符串赋值
str2.assign(str1); // "abcde"

// 用另一个字符串的一个子串赋值
str2.assign(str1, 1, 3); // "bcd"

// 用一个字符串的前一段子串赋值
str2.assign(str1, 2);  // "cde"

// 用几个相同的字符, 赋值
str2.assign(5, 'c');   // "ccccc"
  • 获取系统环境变量
/* getenv example: getting path */
#include <stdio.h>    /* printf */
#include <stdlib.h>   /* getenv */

int main ()
{
  char* pPath;
  pPath = getenv("PATH");
  if (pPath != NULL)
      printf("The current path is: %s",pPath);
  return 0;
}
  • 查找字符在字符串中出现的位置
/* memchr example */
#include <stdio.h>
#include <string.h>

int main ()
{
  char * pch;
  char str[] = "Example string";
  pch = (char*) memchr (str, 'p', strlen(str));
  if (pch!=NULL)
    printf ("'p' found at position %d.\n", pch-str+1);
  else
    printf ("'p' not found.\n");
  return 0;
}

/* 输出: 'p' found at position 5. */
  • 字符数组转换成字符串
std::string byteToHexStr(unsigned char byte_arr[], int arr_len)
{
    std::ostringstream oss("");

    for (int i = 0; i < arr_len; ++i)
    {
        int b = byte_arr[i];
        char str_b[8] = {0};
        sprintf_s(str_b, sizeof(str_b)-1, "%02x", b);
        oss << str_b;
    }
    
    return oss.str();
}

int _tmain(int argc, _TCHAR* argv[])
{
    unsigned char byte_arr[24] = {0x00,0x00, 0x01, 0x02, 0x12, 0x99};

    // ret = "000001021299000000000000000000000000000000000000"
    std::string ret = byteToHexStr(byte_arr, 24);

	return 0;
}
  • C++11获取毫秒
auto timeNow = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch());
unsigned short ms = timeNow.count() % 1000;
  • 格式化日期时间字符串(精确到毫秒)
auto timeNow = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch());
auto ts = timeNow.count() / 1000;
auto tms = timeNow.count() % 1000;
char now[64];
struct tm* ttime = std::localtime(&ts);
strftime(now, 64, "%Y-%m-%d %H:%M:%S", ttime);
std::ostringstream oss("");
oss << now << "." << std::setw(3) << std::setfill('0') << tms;
processTimeNode.append_child(pugi::node_pcdata).set_value(oss.str().c_str());
  • 获取当前线程ID
std::ostringstream oss("");
oss << std::this_thread::get_id();
std::string threadId = oss.str();

并发(Concurrency)编程

与 C++11 多线程相关的头文件

C++11 新标准中引入了五个头文件来支持多线程编程,它们分别是 , , ,

  • :该头文主要声明了两个类, std::atomic 和 std::atomic_flag,另外还声明了一套 C 风格的原子类型和与 C 兼容的原子操作的函数。
  • :该头文件主要声明了 std::thread 类,另外 std::this_thread 命名空间也在该头文件中。
  • :该头文件主要声明了与互斥量(Mutex)相关的类,包括 std::mutex_* 一系列类,std::lock_guard, std::unique_lock, 以及其他的类型和函数。
  • :该头文件主要声明了与条件变量相关的类,包括 std::condition_variable 和 std::condition_variable_any。
  • :该头文件主要声明了 std::promise, std::package_task 两个 Provider 类,以及 std::future 和 std::shared_future 两个 Future 类,另外还有一些与之相关的类型和函数,std::async() 函数就声明在此头文件中。

下面是一个最简单的使用 std::thread 类的例子:

#include <iostream>
#include <thread>

void thread_task()
{
    printf("hello thread!\n");
}

int main()
{
    std::thread t(thread_task);
    t.join();
    return 0;
}

std::thread 各种构造函数例子如下:

#include <iostream>
#include <thread>
#include <utility>
#include <chrono>
#include <functional>
#include <atomic>

void f1(int n)
{
    printf("thread 1 executing...\n");
    for (int i = 0; i < 5; ++i) {
        printf("id = %d, n = %d, i = %d\n", std::this_thread::get_id(), n, i);
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

void f2(int& n)
{
    printf("thread 2 executing...\n");
    for (int i = 0; i < 5; ++i) {
        printf("id = %d, n = %d, i = %d\n", std::this_thread::get_id(), n, i);
        ++n;
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

int main()
{
    int n = 0; 
    std::thread t1;                  // t1 is not a thread
    std::thread t2(f1, n+1);         // pass by value
    std::thread t3(f2, std::ref(n)); // pass by reference
    std::thread t4(std::move(t3));   // t4 is now running, t3 is no longer a thread

    t2.join();
    t4.join();

    printf("final value of n is %d\n", n);

    return 0;
}

注意:可被 joinable 的 std::thread 对象必须在他们销毁之前被主线程 join 或者将其设置为 detached.

Join 线程,调用该函数会阻塞当前线程,直到由 *this 所标示的线程执行完毕 join 才返回。

detach: Detach 线程。将当前线程对象所代表的执行实例与该线程对象分离,使得线程的执行可以单独进行。一旦线程执行完毕,它所分配的资源将会被释放。

std::thread::joinable

bool joinable() const noexcept;  (since C++11)
#include <iostream>
#include <thread>
#include <chrono>
 
void foo()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
int main()
{
    std::thread t;
    // std::boolalpha将bool解析成为单词true, false.
    std::cout << "before starting, joinable: " << std::boolalpha << t.joinable() << '\n';
              
    t = std::thread(foo);
    std::cout << "after starting, joinable: " << t.joinable() << '\n';
              
    t.join();
    std::cout << "after joining, joinable: " << t.joinable() << '\n';
}
// output
before starting, joinable: false
after starting, joinable: true
after joining, joinable: false

std::mutex

This example shows how a mutex can be used to protect a std::map shared between two threads.

#include <iostream>
#include <thread>
#include <chrono>
#include <string>
#include <mutex>
#include <map>

std::map<std::string, std::string> g_pages;
std::mutex g_pages_mutex;

void save_page(const std::string& url)
{
    // simulate a long page fetch
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::string result = "fake content";

    std::lock_guard<std::mutex> guard(g_pages_mutex);
    g_pages[url] = result;
}

int main()
{
    std::thread t1(save_page, "http://foo");
    std::thread t2(save_page, "http://bar");

    t1.join();
    t2.join();

    // safe to access g_pages without lock now, as the threads are joined
    for (const auto& pair : g_pages) {
        std::cout << pair.first << " => " << pair.second << "\n";
    }
}
// output
http://bar => fake content
http://foo => fake content

std::condition_variable

The condition_variable class is a synchronization primitive that can be used to block a thread, or multiple threads at the same time, until another thread both modifies a shared variable (the condition), and notifies the condition_variable.

#include <iostream>
#include <thread>
#include <string>
#include <mutex>
#include <condition_variable>

std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;

void worker_thread()
{
    // wait until main() sends data
    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, [] {return ready;});

    // after the wait, we own the lock.
    std::cout << "Worker thread is processing data...\n";
    data += "after processing";

    // send data back to main()
    processed = true;
    std::cout << "Worker thread signals data processing completed!\n";

    // Manual unlocking is done before notifying, to avoid waking up
    // the waiting thread only to block again (see notify_one for details)
    lk.unlock();
    cv.notify_one();
}

int main()
{
    std::thread worker(worker_thread);
    data = "Example data";
    // send data to the worker thread
    {
        std::lock_guard<std::mutex> lk(m);
        ready = true;
        std::cout << "main() signals data ready for processing.\n";
    }
    cv.notify_one();

    // waite for the worker
    {
        std::unique_lock<std::mutex> lk(m);
        cv.wait(lk, [] {return processed;});
    }

    std::cout << "Back in main(), data = " << data << "\n";

    worker.join();
    return 0;
}

// output
main() signals data ready for processing.
Worker thread is processing data...
Worker thread signals data processing completed!
Back in main(), data = Example dataafter processing

变长参数

变长参数宏

在定义宏的时候也能够像printf一样可以使用变长参数,即宏的参数可以是任意个,这个功能可以由编译器的变长参数宏来实现。

  • 在GCC编译器下,变长参数宏可以使用”##”宏字符串连接操作实现
    #define printf(args...) fprintf(stdout, ##args) 
    
  • 在MSVC下,可以使用__VA_ARGS__这个编译器内置宏
    #define printf(...) fprintf(stdout, __VA_ARGS__) 
    
  • windows ```cpp // test_va.cpp : Defines the entry point for the console application. //

#include “stdafx.h” #include #include #include #include #include

void logv(const char *file, int line, const char *fmt, …) { va_list ap; va_start(ap, fmt); printf(“file: %s \n line: %d: “, file, line); vprintf(fmt, ap); va_end(ap); }

#define dolog(fmt, …) logv(FILE, LINE, fmt, VA_ARGS)

int main(int argc, char *argv[]) { dolog(“argc=%d\n”, argc);

return 0; } ```
#include "stdafx.h"

#define LOG(...) { \
    fprintf(stderr, "[File: %s, Line: %d]:\t", __FILE__, __LINE__); \
    fprintf(stderr, __VA_ARGS__); \
    fprintf(stderr, "\n"); \
}

int _tmain(int argc, _TCHAR* argv[])
{
    int x = 123;
    LOG("x = %d", x);
	return 0;
}
  • linux
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>

#include <stdarg.h>
#include <string>

void logv(const char *file, int line, const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    printf("file: %s \n line: %d \n ", file, line);

    enum {
        MAX_BUF_LLEN = 512,
    };
    char buf[MAX_BUF_LLEN];

    snprintf(buf, MAX_BUF_LLEN-1,  "[FILE: %s, LINE: %d]***", file, line);

    printf("1--> %s\n", buf);

    printf("len = %d\n", strlen(buf));

    vsprintf(buf + strlen(buf), fmt, ap);
    printf("2--> %s\n", buf);

    // vsprintf(buf + strlen(buf), "%s", MAX_BUF_LLEN - 1, "");

    vprintf(fmt, ap);
    va_end(ap);
}

#define dolog(fmt, args...) logv(__FILE__, __LINE__, fmt, ##args)

int main(int argc, char *argv[])
{
    dolog("argc=%d\n", argc);

    return 0;
}

attribute

attribute((format(printf, a, b)))

#ifdef __GNUC__
#define EV_CHECK_FMT(a,b) __attribute__((format(printf, a, b)))
#else
#define EV_CHECK_FMT(a,b)
#endif

功能:

__attribute__((format(printf, a, b)))

属性可以给被声明的函数加上类似printf或者scanf的特征,它可以使编译器检查函数声明和函数实际调用参数之间的格式化字符串是否匹配。format属性告诉编译器,按照printf, scanf等标准C函数参数格式规则对该函数的参数进行检查。这在我们自己封装调试信息的接口时非常的有用。

format的语法格式为:

format (archetype, string-index, first-to-check)

其中,“archetype”指定是哪种风格;“string-index”指定传入函数的第几个参数是格式化字符串;“first-to-check”指定从函数的第几个参数开始按上述规则进行检查。 具体的使用如下所示:

__attribute__((format(printf, a, b)))

__attribute__((format(scanf, a, b)))  

其中参数m与n的含义为:
 a:第几个参数为格式化字符串(format string);
 b:参数集合中的第一个,即参数“…”里的第一个参数在函数参数总数排在第几。

下面直接给个例子来说明:

#include <stdio.h>
#include <stdarg.h> 
#if 1
#define CHECK_FMT(a, b)	__attribute__((format(printf, a, b)))
#else
#define CHECK_FMT(a, b)
#endif 

void TRACE(const char *fmt, ...) CHECK_FMT(1, 2);

void TRACE(const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);
    (void)printf(fmt, ap);
    va_end(ap);
} 

int main(void) {
    TRACE("iValue = %d\n", 6);
    TRACE("iValue = %d\n", "test");
    return 0;
}

注意:需要打开警告信息即(-Wall)。

编译结果如下所示:

main.cpp: In function int main():
main.cpp:26:31: warning: format %d expects argument of type int, but argument 2 has type const char* [-Wformat=]
  TRACE("iValue = %d\n", "test");

如果不使用__attribute__ format则不会有警告。

attribute((noreturn))的用法

#ifdef __GNUC__
#define EV_NORETURN __attribute__((noreturn))
#else
#define EV_NORETURN
#endif
__attribute__((noreturn))   表示没有返回值

C库函数abort()和exit()都使用此属性声明:

extern void exit(int)   __attribute__((noreturn));
extern void abort(void) __attribute__((noreturn));

未使用的情况下,出现警告:

$ cat test1.c
extern void exitnow();

int foo(int n)
{
    if ( n > 0 )
	{
        exitnow();
		/* control never reaches this point */
	}
    else
        return 0;
}

$ cc -c -Wall test1.c
test1.c: In function `foo':
test1.c:9: warning: this function may return with or without a value

使用的情况下,没有警告:

$ cat test2.c
extern void exitnow() __attribute__((noreturn));

int foo(int n)
{
    if ( n > 0 )
        exitnow();
    else
        return 0;
}

$ cc -c -Wall test2.c
no warnings!

Comments

Content