C++ 语法杂记

2023-05-16

文章目录

  • 编译
    • c++中DLL文件的编写与实现
    • 条件编译#ifdef的妙用详解_透彻
  • 语法
    • *a++=*b++;
    • typedef
    • explicit
    • 命名空间
    • C++中子类调用父类的有参构造函数
    • void*
    • 虚继承
    • malloc(), calloc(), recalloc(), free()
    • VECTOR SWAP
    • private
    • 初始化二维向量
    • extern 和 static
    • memset函数详细说明
    • fegets
    • *(int *)
    • 双重指针
    • (void *)1
    • union
    • 堆和栈
    • template
    • strok
    • volatile
    • 函数模板隐式实例化
    • vector
    • 右值引用 &&
    • vector 删除
    • new 和 malloc区别
    • new[] delete[]
    • unique_ptr
    • 循环引用
    • incline
    • __restrict__
    • lambda
    • friend
    • std::hash
    • *p++
    • c 字符串操作
    • errno.h
    • #define
    • null,0,'\0'
    • va_arg() 宏
    • 零长数组
    • weak_ptr, shared_ptr
    • 成员指针
    • operator->
    • vector 内存分配
    • line
    • new 操作
    • c 浮点
    • 对齐
    • sscanf
    • const
    • 整数
    • 类初始化
    • 模板声明和定义分离
    • future promise
    • 运算符重载
    • memmove
    • 字符串中的双引号
    • 宏 do while
    • ::
    • 参数列表和右值
    • 类型自动转换
    • decltype
    • 大括号
    • 浅拷贝
    • string_view
    • string &&
    • make_shared
    • `emplace`_back
    • nullptr和NULL
    • map 遍历删除
    • unique_lock
    • 派生类使用基类的构造函数
    • wait_for
    • std::atomic
    • td::ios:app 和 seekp
    • 自定义排序
    • 自定义set
    • 模板的模板参数
    • 类模板和友元
    • 模版类不同具化类之间的转换
  • 模板递归
    • 模板的隐式推导
    • 自定义类型的前置声明
    • SFINAE
    • 类static成员变量初始化
    • std::initializer_list
    • 一个简单的反射系统
    • 偏特化应用
    • 虚析构
  • windows迁移到linux
  • 可变参数函数模板
  • operator()
  • 前缀式和后缀式自增和自减
  • ptrdiff_t
  • realloc

编译

c++中DLL文件的编写与实现

link.

条件编译#ifdef的妙用详解_透彻

link.

语法

*a++=*b++;

*a++=*b++;
的确是相当于
*(a++)=*(b++);
由于++在后
所以

又相当于
*a=*b;
a++;
b++;

typedef

ypedef的作用有以下几点:

1)typedef的一个重要用途是定义机器无关的类型。例如,定义一个叫REAL的浮点类型,该浮点类型在目标机器上可以获得最高的精度:

typedef long double REAL;

如果在不支持 long double 的机器上运行相关代码,只需要对对应的typedef语句进行修改,例如:

typedef double REAL;

或者:

typedef float REAL;

2)使用typedef为现有类型创建别名,给变量定义一个易于记忆且意义明确的新名字。例如:

typedef unsigned int UINT

3)使用typedef简化一些比较复杂的类型声明,例如:

typedef void (*PFunCallBack)(char* pMsg, unsigned int nMsgLen);

上述声明引入了PFunCallBack类型作为函数指针的同义字,该函数有两个类型分别为char*和unsigned int参数,以及一个类型为int的返回值。通常,当某个函数的参数是一个回调函数时,可能会用到typedef简化声明。

例如,承接上面的示例,我们再列举下列示例:

RedisSubCommand(const string& strKey, PFunCallBack pFunCallback, bool bOnlyOne);

也可以直接自定义函数,调用的时候标识后加*代表函数指针

typedef long SetStringPtr(char *);
SetStringPtr* SetString;

explicit

explicit关键字的作用就是防止类构造函数的隐式自动转换.

上面也已经说过了, explicit关键字只对有一个参数的类构造函数有效, 如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以explicit关键字也就无效了. 例如:

 class CxString  // 使用关键字explicit的类声明, 显示转换  
{  
public:  
    char *_pstr;  
    int _size;  
    explicit CxString(int size)  
    {  
        _size = size;  
        // 代码同上, 省略...  
    }  
    CxString(const char *p)  
    {  
        // 代码同上, 省略...  
    }  
};  

CxString string1(24);     // 这样是OK的  
CxString string2 = 10;    // 这样是不行的, 因为explicit关键字取消了隐式转换  
CxString string3;         // 这样是不行的, 因为没有默认构造函数  
CxString string4("aaaa"); // 这样是OK的  
CxString string5 = "bbb"; // 这样也是OK的, 调用的是CxString(const char *p)  
CxString string6 = 'c';   // 这样是不行的, 其实调用的是CxString(int size), 且size等于'c'的ascii码, 但explicit关键字取消了隐式转换  
string1 = 2;              // 这样也是不行的, 因为取消了隐式转换  
string2 = 3;              // 这样也是不行的, 因为取消了隐式转换  
string3 = string1;        // 这样也是不行的, 因为取消了隐式转换, 除非类实现操作符"="的重载  

命名空间

C++命名空间的使用:
使用整个命名空间:using namespace name;
使用命名空间中的变量:using name::variable
使用默认命名空间中的变量: ::variable

C++中子类调用父类的有参构造函数

#include <iostream>
using namespace std;
 
class A
{
  public:
  int a;
  int b;
  A()
  {
    cout<<"A Constructed 1\n";
  }
 
  A(int a,int b)
  {
    this->a=a;
    this->b=b;
    cout<<"A Constructed 2\n";
    cout<<"this.a is "<<this->a<<" this.b is "<<this->b<<"\n";
  }
 
};
 
class B:A
{
 
  public:
  int c;
  int d;
 
  B()
  {
    cout<<"B Constructed 1\n";
  }
 
  B(int c,int d):A(100,200)
  {
    this->c=c;
    this->d=d;
    cout<<"B Constructed 2\n";
  }
};
 
int main()
{
  B b(1,1);
  cout<<"b.c is "<<b.c<<" b.d is "<<b.d <<"\n";
  return 1;
}

void*

typedef struct student_tag
{
    char name[STU_NAME_LEN];  //学生姓名
    unsigned int id;          //学生学号
    int score;                //学生成绩
}student_t;
int studentCompare(const void *stu1,const void *stu2)
{
  /*强转成需要比较的数据结构*/
    student_t *value1 = (student_t*)stu1;
    student_t *value2 = (student_t*)stu2;
    return value1->score-value2->score;
}

虚继承

用于应对菱形继承情况下,派生类从多个路径继承基类的成员变量造成的命名冲突问题:

    //间接基类A
    class A{
    protected:
        int m_a;
    };
    //直接基类B
    class B: virtual public A{  //虚继承
    protected:
        int m_b;
    };
    //直接基类C
    class C: virtual public A{  //虚继承
    protected:
        int m_c;
    };
    //派生类D
    class D: public B, public C{
    public:
        void seta(int a){ m_a = a; }  //正确
        void setb(int b){ m_b = b; }  //正确
        void setc(int c){ m_c = c; }  //正确
        void setd(int d){ m_d = d; }  //正确
    private:
        int m_d;
    };
    int main(){
        D d;
        return 0;
    }

malloc(), calloc(), recalloc(), free()

1、malloc()

头文件:stdlib.h

声明:void * malloc(int n);

含义:在堆上,分配n个字节,并返回void指针类型。

返回值:分配内存成功,返回分配的堆上存储空间的首地址;否则,返回NULL

2、calloc()

头文件:stdlib.h

声明:void *calloc(int n, int size);

含义:在堆上,分配nsize个字节,并初始化为0,返回void 类型

返回值:同malloc() 函数

3、recalloc()

头文件:stdlib.h

声明:void * realloc(void * p,int n);

含义:重新分配堆上的void指针p所指的空间为n个字节,同时会复制原有内容到新分配的堆上存储空间。注意,若原来的void指针p在堆上的空间不大于n个字节,则保持不变。

返回值:同malloc() 函数

4、free()

头文件:stdlib.h

声明:void free (void * p);

含义:释放void指针p所指的堆上的空间。

返回值:无

5、memset()

头文件:string.h

声明:void * memset (void * p, int c, int n) ;

含义:对于void指针p为首地址的n个字节,将其中的每个字节设置为c。

返回值:返回指向存储区域 p 的void类型指针。

VECTOR SWAP

给扩容后的向量重新分配适合的内存大小
vector(v) 新建一个capacity等于v的size的匿名向量
swap交换空间
假如向量v中有很多空闲空间

vector<int>(v).swap(v);  

private

字段的private修饰符基于类的,类的方法可以访问这个方法的类的任何对象的private字段

初始化二维向量

n不能为一个表达式
vector<vector>vec(m,vector(n,0));

extern 和 static

我们知道,程序的编译单位是源程序文件,一个源文件可以包含一个或若干个函数。在函数内定义的变量是局部变量,而在函数之外定义的变量则称为外部变量,外部变量也就是我们所讲的全局变量。它的存储方式为静态存储,其生存周期为整个程序的生存周期。全局变量可以为本文件中的其他函数所共用,它的有效范围为从定义变量的位置开始到本源文件结束。

然而,如果全局变量不在文件的开头定义,有效的作用范围将只限于其定义处到文件结束。如果在定义点之前的函数想引用该全局变量,则应该在引用之前用关键字 extern 对该变量作“外部变量声明”,表示该变量是一个已经定义的外部变量。有了此声明,就可以从“声明”处起,合法地使用该外部变量
1、函数的声明extern关键词是可有可无的,因为函数本身不加修饰的话就是extern。但是引用的时候一样需要声明的。
2、全局变量在外部使用声明时,extern关键字是必须的,如果变量没有extern修饰且没有显式的初始化,同样成为变量的定义,因此此时必须加extern,但是一個全局变量只允许有一个定义,而编译器在此标记存储空间在执行时加载内并初始化为0。而局部变量的声明不能有extern的修饰,且局部变量在运行时才在堆栈部分分配内存。

3、全局变量或函数本质上讲没有区别,函数名是指向函数二进制块开头处的指针。而全局变量是在函数外部声明的变量。函数名也在函数外,因此函数也是全局的。

    /****max.c****/
    #include <stdio.h>
    /*外部变量声明*/
    extern int g_X ;
    extern int g_Y ;
    int max()
    {
        return (g_X > g_Y ? g_X : g_Y);
    }
    /***main.c****/
    #include <stdio.h>
    /*定义两个全局变量*/
    int g_X=10;
    int g_Y=20;
    int max();
    int main(void)
    {
        int result;
        result = max();
        printf("the max value is %d\n",result);
        return 0;
    }

一般局部变量是存储在栈区的,局部变量的生命周期在其所在的语句块执行结束时便结束了。但如果用static修饰局部变量,那么这个变量就不会存储在栈区而是放在静态数据区,其生命周期会一直持续到整个程序结束,该变量只在初次运行时进行初始化,且只进行一次,但是它的作用域只能是在函数里面
众所周知,static变量只能在定义它的文件中使用。这里的文件指的是cpp源文件。如果在头文件中定义了static变量,那么,所有包含这个头文件的源文件都会定义自己的static变量,而不是使用该头文件中的static变量。
将static全局变量写在头文件中,所有文件的头文件的操作都会共享这个变量。
但如果是在源文件(cpp)中去操作这个静态全局变量,则这个静态全局变量只能在当前文件有效,但是在另外一个文件访问此静态变量,会是该变量初始的默认值,不会是其他文件中修改的值,虽然它们有相同的初始内容,但是存储的物理地址并不一样
所以一般定义static全局变量时,都把它放在原文件中而不是头文件,从而避免多个源文件共享,就不会给其他模块造成不必要的信息污染。如果想要在不同文件共享同一个全局变量,这个时候就要用到extern。

在头文件中用static incline 修饰一个函数,那么包含这个头文件的文件都会有这个函数的副本,在使用时直接展开。

在函数的返回类型前加上关键字static,函数就被定义成为静态函数。
函数的定义和声明默认情况下是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用

memset函数详细说明

1)void *memset(void *s,int c,size_t n)
总的作用:将已开辟内存空间 s 的首 n 个字节的值设为值 c。

2).memset() 函数常用于内存空间初始化。如:
char str[100];
memset(str,0,100);

3).memset可以方便的清空一个结构类型的变量或数组。
如:

   struct sample_struct{
              char csName[16];
               int iSeq;
               int iType;
   };

对于变量:
struct sample_strcut stTest;
一般情况下,清空stTest的方法:

           stTest.csName[0]='/0';
           stTest.iSeq=0;
           stTest.iType=0;

用memset就非常方便:

memset(&stTest,0,sizeof(struct sample_struct));

fegets

# include <stdio.h>
int main(void)
{
    char str[30];
    char ch;
    printf("请输入字符串:");
    fgets(str, 5, stdin);
    printf("%s", str);  //后面不要加'\n'
    scanf("%c", &ch);
    printf("ch = %c\n", ch);
    return 0;
}

输出

1234ch = 5

*(int *)

在这里插入图片描述

双重指针

```cpp
char GetMemory(char **memorystr,u8 num)// 使用二级指针
{
    *memorystr = (char*)malloc(num);// 这里是赋值给*memorystr 
}

int main(void)
{
    char *str = NULL;
    GetMemory(&str,6);// 取str的地址
    memcpy(str,"china",5);
} 

(void *)1

#include <iostream>
#include <string>

using namespace std;

int main ()
{
 int a=3;
 void *b;
 b=(void*)222;
 cout<<(long)b;
}

由于使用了强转将void转为int,而在XCode上使用的为x64的编译,X64下的void地址为8字节,而int为4字节,强转时会导致越界,故这里必须使用(long)。

union

union myun 
{
struct { int x; int y; int z; }u; 
int k; 
}a; 
int main() 
{ 
a.u.x =4;
a.u.y =5; 
a.u.z =6; 
a.k = 0; 
printf("%d %d %d\n",a.u.x,a.u.y,a.u.z);
return 0;
}

输出0,5,6

堆和栈

在这里插入图片描述

template

template <typename TYPE, void (TYPE::*RunGuiLoop)() >
TYPE: : * 是一个指向TYPE成员数据的指针
RunGuiLoop 是指向成员函数的指针

下面是用于交换job结构的非模板函数、模板函数和具体化的原型:

void Swap(job &, job &)

template <typename T> 
void Swap(T & , t &);

template <> void Swap<job>(job &, job &);

非模板优于具体化优于常规模板。

当使用关键字template 并指出所需类型来声明类时,编译器将生成类声明的显示实例化

template class ArrayTP<string,100>; 

strok

//借助strtok实现split
#include <string.h>
#include <stdio.h>
 
int main()
{
    char s[] = "Hello world,engineers!~";
    const char *d = " ,!";//以空格,逗号和感叹号作为分隔符,注意是可以取多个的
    char *p;
    p = strtok(s,d);
    while(p)
    {
        printf("%s\n",p);
        p=strtok(NULL,d);
    }
 
    return 0;
}

第一次调用时候,strtok函数从不是分隔符的第一个字符开始搜索,找到第一个是分隔符为止,将其替换为‘\0’作为结束。但是函数此时已经保存了分隔符之后的那个字符,因此
后续再调用strtok时,第一个参数为NULL,继续将string标记化(切割)。NULL参数表示调用strtok继续从string中上次调用 strtok时保存的位置开始标记化。

volatile

volatile的本质是编译器的优化

在本次线程内, 当读取一个变量时,为提高存取速度,编译器优化时有时会先把变量读取到一个寄存器中;以后,再取变量值时,就直接从寄存器中取值;当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以便保持一致。

当变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致。

当该寄存器在因别的线程等而改变了值,原变量的值不会改变,从而造成应用程序读取的值和实际的变量值不一致。

函数模板隐式实例化

函数模板隐式实例化指的是在发生函数调用的时候,如果没有发现相匹配的函数存在,编译器就会寻找同名函数模板,如果可以成功进行参数类型推演,就对函数模板进行实例化。
还有一种简介调用函数的情况,也可以完成函数模板的实例化。所谓的简介调用是指将函数入口地址传给一个函数指针,通过函数指针完成函数调用。如果传递给函数指针不是一个真正的函数,那么编译器就会寻找同名的函数模板进行参数推演,进而完成函数模板的实例化。

#include <iostream>
using namespace std;
template <typename T> void func(T t){
    cout<<t<<endl;
}

void invoke(void (*p)(int)){
    int num=10;
    p(num);
}
int main(){
    invoke(func);
}

vector

vector 的reserve增加了vector的capacity,但是它的size没有改变!而resize改变了vector的capacity同时也增加了它的size!

每次push_back的时候,在相应的位置处使用placement new,调用元素类的拷贝构造函数,复制出一个副本对象来

右值引用 &&

 void process_value(int& i) 
 { 
  std::cout << "LValue processed: " << i << std::endl; 
 } 
 void process_value(int&& i) 
{ 
  std::cout << "RValue processed: " << i << std::endl; 
 } 

 void forward_value(int&& i) { 
  process_value(i); 
 } 

 int main() 
 { 
 int a = 0; 
 process_value(a); 
 process_value(1); 
 forward_value(2);
运行结果 :
LValue processed: 0
RValue processed: 1
LValue processed: 2 

函数返回的内部变量就是右值,只能在函数外赋值给左值,而且相当与重新拷贝一份。

在C++11中,我们可以显示地使用“右值引用”来绑定一个右值,语法是"&&"。因为指定了是右值引用,所以无论是否const都是正确的。

const string&& name = getName(); // ok  
string&& name = getName(); // also ok   
 const int& val = getVal();//right  
int& val = getVal();//error 
std::move

这条语句可以将左值转换为右值,可以用在函数的返回值
而右值返回值可用于类的转移构造函数

拷贝构造函数的调用时机:

  1. 当函数的参数为类的对象时
  2. 函数的返回值是类的对象
    CExample g_fun()
    {
        CExample temp(0);
        return temp;
    }
    int main()
    {
        
        g_fun();
        return 0;
    }
    
    当g_Fun()函数执行到return时,会产生以下几个重要步骤:
    (1). 先会产生一个临时变量,就叫XXXX吧。
    (2). 然后调用拷贝构造函数把temp的值给XXXX。整个这两个步骤有点像:CExample XXXX(temp);
    (3). 在函数执行到最后先析构temp局部变量。
    (4). 等g_fun()执行完后再析构掉XXXX对象。
  3. 对象需要通过另外一个对象继续初始化

一个转移赋值的例子:

MyString& operator=(MyString&& str) { 
   std::cout << "Move Assignment is called! source: " << str._data << std::endl; 
   if (this != &str) { 
     _len = str._len; 
     _data = str._data; 
     str._len = 0; 
     str._data = NULL; 
   } 
   return *this; 
}
B&& f()//注意这里B&&
{
    B tmp;
    return std::move(tmp);
}

B& f2()//注意这里的B&
{
    B t;
    return t;
}
int main()
{
 B b0(f2());
 B b1(f()):
return 0 ;
}

假设b0有转移构造函数和拷贝构造函数,那么以上的b0(f2())和 B b1(f())都会发生错误,f()会返回一个右值的引用_tmp,但右值在函数返回时被析构。f2中返回一个左值的引用,但左值也会被析构。但是如果对象出了函数作用域也不会析构,可以返回引用。
正确做法:

B f()
{
    B tmp;
    return std::move(tmp);
}

临时变量_tmp通过移动构造函数构建,并作为右值传给b1,使得b1也调用移动构造函数。
右值可以简单理解为没有地址的值,右值的意义只是为了编译器的优化,例如:

struct foo{};
foo bar() 
{
    foo _f;
    return _f;
}
void dummy(foo f) 
{
}
int main() 
{
    dummy(bar());
}

编译器会自动把f的地址给bar()。将亡值是给右值一个临时的地址,比如右值引用,和move操作过的左值。一般左值引用时,不希望破坏左值,所以以左值为参数时使用拷贝构造函数。而右值可以随便破坏,所以以右值引用为参数时可以直接将其资源接管过来。

vector 删除

在这里插入图片描述
如果在 remove() 操作后输出 words 中的元素,只会输出前 5 个元素。尽管 size() 返回的值仍然是 7,而且最后两个元素仍然存在,但是它们被替换成了空字符串对象。为了摆脱这些多余的元素,可以使用成员函数 erase()。remove() 返回的迭代器可以这样使用:

words.erase(iter, std::end(words));//Remove surplus elements

new 和 malloc区别

2.1 属性
new和delete是C++关键字,需要编译器支持;malloc和free是库函数,需要头文件支持。

2.2 参数
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。

2.3 返回类型
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。

2.4 自定义类型
new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。

malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。

2.5 “重载”
C++允许自定义operator new 和 operator delete 函数控制动态内存的分配。

2.6 内存区域
new做两件事:分配内存和调用类的构造函数,delete是:调用类的析构函数和释放内存。而malloc和free只是分配和释放内存。

new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。

2.7 分配失败
new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。

2.8 内存泄漏
内存泄漏对于new和malloc都能检测出来,而new可以指明是哪个文件的哪一行,malloc确不可以。

new[] delete[]

class A *pAa = new class A[3];
delete pAa;

如果仔细看过上面的图,可能会有疑惑:new最后将开辟好内存用指针p返回,pA接收它。可为什么p 和pA 会差上4字节?

这其实是因为编译器用相差的这4个字节用来保存一个东西——对象个数,即AA* p = new AA[10] 中的‘10’。这也就不难解释 为什么在delete[] 的时候,不用传给它对象个数。

对指针重复使用delete可能会导致错误

通过new 得到的数组,如果里面保存的是对象,delete数组时,每个对象都会调用析构函数。但是如果数组里保存的是对象指针,就不会调用析构函数。

unique_ptr

unique

例子:

unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1;                                      // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You"));  // 允许,因为右值是临时变量

函数返回右值可以赋值给unique_ptr变量。

循环引用

原理

由于类A需要使用到类B对象,而类B中也需要使用到类A对象,如此声明,头文件循环引用,编译A.cpp或B.cpp时由于文件引用将头文件内容拷贝至引用位置,拷贝后又发现存在头文件引用,因此会持续该行为,由于有循环引用,该行为不会停止,为了避免循环引用导致编译陷入死循环,编译器会检测文件引用,当超过一定深度时,会提示错误。

解决

头文件A.h中对象b只做声明作用,并不会直接调用对象b的接口,因此在头文件A.h中不直接引用B.h,只需前置声明类B,代码文件A.cpp中由于会使用到类B的接口,因此需要引用类B的头文件,例如:

incline

1. inline 定义的类的 内联函数,函数的代码被放入 符号表中,在使用时直接进行替换,(像宏一样展开),没有了调用的开销,效率也很高。
2. 很明显,类的内联函数也是一个真正的函数, 编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确。然后进行一系列的相关检查,就像对待任何一个真正的函数一样。这样就消除了它的隐患和局限性。
3. inline 可以作为某个 类的成员函数,当然就可以在其中使用所在类的保护成员及私有成员。

restrict

int rfoo(int *restrict a, int *restrict b)
{
    *a = 5;
    *b = 6;
    return *a + *b;
} 

总括而言,restrict 是为了告诉编译器额外信息(两个指针不指向同一数据),从而生成更优化的机器码。注意,编译器是无法自行在编译期检测两个指针是否 alias。如使用 restrict,程序员也要遵守契约才能得出正确的代码(指针不能指向相同数据)

lambda

C++11 的 lambda 表达式规范如下:
[ capture ] ( params ) mutable exception attribute -> ret { body } (1)
[ capture ] ( params ) -> ret { body } (2)
[ capture ] ( params ) { body } (3)
[ capture ] { body } (4)
在这里插入图片描述

其中

(1) 是完整的 lambda 表达式形式,
(2) const 类型的 lambda 表达式,该类型的表达式不能改捕获(“capture”)列表中的值。
(3)省略了返回值类型的 lambda 表达式,但是该 lambda 表达式的返回类型可以按照下列规则推演出来:
如果 lambda 代码块中包含了 return 语句,则该 lambda 表达式的返回类型由 return 语句的返回类型确定。
如果没有 return 语句,则类似 void f(…) 函数。
省略了参数列表,类似于无参函数 f()。

mutable 修饰符说明 lambda 表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获对象的 non-const 方法。

exception 说明 lambda 表达式是否抛出异常(noexcept),以及抛出何种异常,类似于void f() throw(X, Y)。

attribute 用来声明属性。

另外,capture 指定了在可见域范围内 lambda 表达式的代码内可见得外部变量的列表,具体解释如下:
在这里插入图片描述
此外,params 指定 lambda 表达式的参数。
lambda函数的类型
在这里插入图片描述

friend

类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。

如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend

std::hash

std::hash<int> hash_int;// Function object to hash int
std::vector<int> n{-5, -2, 2, 5, 10};
std::transform(std::begin(n), std::end(n),std::ostream_iterator<size_t> (std:: cout,"=="),hash_int);

输出:

18446744073709551611==18446744073709551614==2==5==10==

*p++

*p++ 唯一不同的一个,bai先取p地址处的值,再对p执行+1

c 字符串操作

  • memchr 在指定内存里定位给定字符

  • strchr 在指定字符串里定位给定字符

  • strcspn 返回在字符串str1里找到字符串str2里的任意一个字符之前已查找的字符数量

  • strrchr 在字符串里定位给定字符最后一次出现的位置

  • strpbrk 在字符串str1里定位字符串str2里任意一个首次出现的字符

  • strspn 返回字符串str1从开始字符到第一个不在str2中的字符个数

  • strstr 在字符串str1中定位字符串str2首次出现的位置

strcasecmp用忽略大小写比较字符串

  • strtol(const char *str, char *endptr, int base)
    str – 要转换为长整数的字符串。
    endptr – 对类型为 char
    的对象的引用,其值由函数设置为 str 中数值后的下一个字符。
    base – 基数,必须介于 2 和 36(包含)之间,或者是特殊值 0。
  • int putchar(int char) 把参数 char 指定的字符(一个无符号字符)写入到标准输出 stdout 中
    必需。规定字符串以及如何格式化其中的变量。

sprintf(format,arg1,arg2,arg++)的格式值:

%% - 返回一个百分号 %
%b - 二进制数
%c - ASCII 值对应的字符
%d - 包含正负号的十进制数(负数、0、正数)
%e - 使用小写的科学计数法(例如 1.2e+2)
%E - 使用大写的科学计数法(例如 1.2E+2)
%u - 不包含正负号的十进制数(大于等于 0)
%f - 浮点数(本地设置)
%F - 浮点数(非本地设置)
%g - 较短的 %e 和 %f
%G - 较短的 %E 和 %f
%o - 八进制数
%s - 字符串
%x - 十六进制数(小写字母)
%X - 十六进制数(大写字母)

附加的格式值。必需放置在 % 和字母之间(例如 %.2f):

+ (在数字前面加上 + 或 - 来定义数字的正负性。默认情况下,只有负数才做标记,正数不做标记)
' (规定使用什么作为填充,默认是空格。它必须与宽度指定器一起使用。例如:%'x20s(使用 "x" 作为填充))
- (左调整变量值)
[0-9] (规定变量值的最小宽度)
.[0-9] (规定小数位数或最大字符串长度)

注释:如果使用多个上述的格式值,它们必须按照以上顺序使用。

errno.h

头文件errno.h中定义了外部变量errno和一些错误宏(i.e. ERANGE),库math.h中的函数遇到错误的时候,比如计算结果用double放不下,就会把错误代码存入变量errno里。在程序中引入头文件errno.h并检查errno就可以检测到这个错误。
C语言预定义了一些错误码和对应的错误信息,perror(msg)根据errno的当前值把预定义的错误信息拼接到msg末尾输出到stderr。

// file: test.c
// 
#include <stdio.h>
#include <errno.h>
#include <math.h>

int main(void){
    errno = 0;
    exp(1e10);

    if (errno == ERANGE)
        perror("Error occured");
    else printf("no error\n");
    return 0;
}
test>$ gcc test.c
test>$ ./a
Error occured: Numerical result out of range

一个调试宏

#ifndef __dbg_h__
#define __dbg_h__

#include <stdio.h>
#include <errno.h>
#include <string.h>

#ifdef NDEBUG
#define debug(M, ...)
#else
#define debug(M, ...) fprintf(stderr, "DEBUG %s:%d: " M "\n", __FILE__, __LINE__, ##__VA_ARGS__)
#endif

#define clean_errno() (errno == 0 ? "None" : strerror(errno))

#define log_err(M, ...) fprintf(stderr, "[ERROR] (%s:%d: errno: %s) " M "\n", __FILE__, __LINE__, clean_errno(), ##__VA_ARGS__)

#define log_warn(M, ...) fprintf(stderr, "[WARN] (%s:%d: errno: %s) " M "\n", __FILE__, __LINE__, clean_errno(), ##__VA_ARGS__)

#define log_info(M, ...) fprintf(stderr, "[INFO] (%s:%d) " M "\n", __FILE__, __LINE__, ##__VA_ARGS__)

#define check(A, M, ...) if(!(A)) { log_err(M, ##__VA_ARGS__); errno=0; goto error; }

#define sentinel(M, ...)  { log_err(M, ##__VA_ARGS__); errno=0; goto error; }

#define check_mem(A) check((A), "Out of memory.")

#define check_debug(A, M, ...) if(!(A)) { debug(M, ##__VA_ARGS__); errno=0; goto error; }

#endif

其中M是一个format,debug的使用方法是debug(“format”, arg1, arg2)
error: 是自定义函数中的一个跳转标签

#define

宏函数中最后一个表达式的值,即为宏函数的返回值。该值的类型,即为宏函数的返回类型。因此,可以说宏函数隐式地指名了其返回值与返回类型。

define中的特殊标识符

#define  Conn(x,y) x##y
#define  ToChar(x) #@x
#define  ToString(x) #x

int  a = Conn( 12 , 34 );
char  b = ToChar(a);
char  c[] = ToString(a);

结果是 a=1234,b=‘a’,c=“1234”; #符号让参数变成参数值的字符串
#@是单引号,#是双引号

 #define  A 3
#define  B 4
#define  AB 5
#define  Conn(x,y) x##y

int  a = Conn(A,B);

结果是a=5,因为有##符号,先展开外面的宏,然后后分析参数。即Conn(A,B)=>AB,后AB=>5

#define f(a, b) a##b
#define g(a) #a, f(a, 0)
#define h(a) _##a, f(a, 0)
#define e(a) a
// 调用
g(e(1)) // "e(1)", 10
h(e(1)) // _e(1), 10
#include <stdio.h>

#define STR(x)			#x
#define TO_STR(x)		STR(x)
#define DEF_VAR(a)		var_##a
#define PARAM(x)		param_##x

void main(void)
{
    printf("%s\r\n", TO_STR(DEF_VAR(PARAM(10))));
}

输出:

var_PARAM(10)

由于DEF_VAR中带有##所以会先将里面的内容使用##拼接,所以会最先展开为TO_STR(var_PARAM(10)),由于展开后PARAM(10)已经变成了var_PARAM(10),已经不是有效的宏了,所以最终再经过TO_STR的转换后,结果就是var_PARAM(10)

嵌套宏的展开规律

一般展开规律为:先展开参数,再分析函数,由内向外展开
当宏中有#运算符的时候,不展开参数
当宏中有##运算符的时候,先展开函数,再分析参数
##运算符用于将参数连接到一起,预处理过程把出现在##运算符两侧的参数合并成一个符号,注意不是字符串

用 gcc -E test.c输出宏展开

null,0,‘\0’

在C语言中,

NULL和0的值都是一样的,但是为了目的和用途及容易识别的原因,NULL用于指针和对象,0用于数值

对于字符串的结尾,使用’\0’,它的值也是0,但是让人一看就知道这是字符串的结尾,不是指针,也不是普通的数值

va_arg() 宏

声明

下面是 va_arg() 宏的声明。

type va_arg(va_list ap, type)

参数

  • ap – 这是一个 va_list 类型的对象,存储了有关额外参数和检索状态的信息。该对象应在第一次调用 va_arg 之前通过调用 va_start 进行初始化。
  • type – 这是一个类型名称。该类型名称是作为扩展自该宏的表达式的类型来使用的。

返回值

该宏返回下一个额外的参数,是一个类型为 type 的表达式。
实例

下面的实例演示了 va_arg() 宏的用法。

#include <stdarg.h>
#include <stdio.h>

int sum(int, ...);

int main()
{
   printf("15 和 56 的和 = %d\n",  sum(2, 15, 56) );
   return 0;
}

int sum(int num_args, ...)
{
   int val = 0;
   va_list ap;
   int i;

   va_start(ap, num_args);
   for(i = 0; i < num_args; i++) 
   {
      val += va_arg(ap, int);
   }
   va_end(ap);
 
   return val;
}

让我们编译并运行上面的程序,这将产生以下结果:

1556 的和 = 71

零长数组

struct buffer
{
    int data_len;   //长度
    char data[0];  //起始地址
};
int main()
{

   struct buffer* first=(struct buffer *)malloc(sizeof(struct buffer)*5);

   first->data[3]='f';
   printf("first_size %d , siezeof buffer %d",sizeof(first),sizeof(buffer));
   return 0;
}

输出

first_size 8 , siezeof stu 4

可以看出data[0]本身并不占结构体大小,但是可以把malloc多出结构体大小的部分分配给data。

weak_ptr, shared_ptr

shared_ptr无法防止循环引用,若我们用同一个普通指针去初始化两个shared_ptr,此时两个ptr均指向同一片内存区域,但是引用计数器均为1,使用时需要注意

#include <iostream>
#include <sstream>
#include <string>
#include <memory>
#include <thread>
#include <chrono>
#include <mutex>
using namespace std;


template<class T>
class SharedPtr
{
public:
        SharedPtr(T*ptr=NULL)
               :_ptr(ptr)
               , _pcount(new int(1))
        {
        cout<<"默认初始化"<<endl;

}
        SharedPtr(SharedPtr&s)
        {cout<<"复制初始化"<<endl;
_ptr = s._ptr;
_pcount = s._pcount;
               ++(*_pcount);
        }
        SharedPtr<T>&operator=(SharedPtr&s)
        {
               if (this!= &s)
               {
cout<<"赋值初始化"<<endl;
                       if (--(*(this->_pcount)) == 0)
                       {
                              delete this->_ptr;
                              delete this->_pcount;
                       }
                       _ptr = s._ptr;
                       _pcount = s._pcount;
                      ++(*_pcount);
               }
               return *this;
        }
        T&operator*()
        {
               return *(this->_ptr);
        }
        T*operator->()
        {
               return this->_ptr;
        }

        int use_count(){
	   return *(this->_pcount);
	}
        ~SharedPtr()
        {
               --(*(this->_pcount));
               if (this->use_count() == 0)
               {
                       delete _ptr;
                       _ptr = NULL;
                       delete _pcount;
                       _pcount = NULL;
               }
        }
public:
        T*_ptr;

      // 
        int *_pcount;//指向引用计数的指针
	//SharedPtr<int> p3;
};

weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include"boost/shared_ptr.hpp"
 
 
struct Node
{
    Node(int value)
    :_value(value)
    {
        cout << "Node()" << endl;
    }
    ~Node()
    {
        cout << "~Node()" << endl;
    }
    shared_ptr<Node> _prev;
    shared_ptr<Node> _next;
    int _value;
};
 
void Test2()
{
    shared_ptr<Node> sp1(new Node(1));
    shared_ptr<Node> sp2(new Node(2));
    cout << sp1.use_count() << endl;
    cout << sp2.use_count() << endl;
    sp1->_next = sp2;
    sp2->_prev = sp1;
    cout << sp1.use_count() << endl;
    cout << sp2.use_count() << endl;
}
 
int main()
{
    Test2();
    return 0;
}

由于先构造的后释放,后构造的先释放可知,先释放的是sp2,那么因为它的引用计数为2,减去1之后就成为了1,不能释放空间,因为还有其他的对象在管理这块空间。但是sp2这个变量已经被销毁,因为它是栈上的变量,但是sp2管理的堆上的空间并没有释放。
接下来释放sp1,同样,先检查引用计数,由于sp1的引用计数也是2,所以减1后成为1,也不会释放sp1管理的动态空间。
通俗点讲:就是sp2要释放,那么必须等p1释放了,而sp1要释放,必须等sp2释放,所以,最终,它们两个都没有释放空间。

解決方法是用weak_ptr替代shared_ptr,然后把_next或_pre其中一个改为weak_ptr。因为weak_ptr 的构造和析构不会增加和减少引用次数。
注意的是我们不能通过weak_ptr直接访问对象的方法,为它本身不会增加引用计数,所以它指向的对象可能在它用的时候已经被释放了,所以在用之前需要使用expired函数来检测是否过期,然后使用lock函数来获取其对应的shared_ptr对象,然后进行后续操作

一个类的的实例在离开作用域时会自动调用类的析构函数,类似int 之类的成员变量会自动delete掉,而通过new赋值或初始化出来的变量需要手动在析构函数里delete掉。

成员指针

class CA
{
public:
int m_i0;
private:
int m_i1;
}

可以这样定义“成员指针”:

int CA:: * pm = &CA::m_i0;

当定义了 CA 的变量、指针,就可以使用上面两个运算符了,例如:

CA a;
CA * p = &a;
a.*pm = 5;

operator->

->和->*都是C++中定义的可重载的运算符,其中:->称为成员选择符(member selection),而->*称为成员指针选择符(pointer-to-member selection)。
对于一个类对象A,对->的调用即A->,相当于(A.operator->)->

vector 内存分配

vector中元素与待插入元素不是同一个对象:

  因为vector使用的alloc(用于分配内存)一直是从堆或者内存池(内存池应该是也属于堆)中获取内存,所以vector中所有的元素肯定不是位于栈上,就是我有一个int a=3,那么a肯定是位于栈上的,但是vector<int>b;b.push_back(a);之后,b[0]虽然值等于a,但是和a已经不是一个对象了,vector会在堆上给其重新分配内存。

push_back时,如果对象没有拷贝构造函数,编译器会为其生成一个,但是这个编译器生成的拷贝构造函数只是进行了一次浅拷贝,

line

c++预处理命令 #line 用法

语法:

#line line_number “filename”

#line命令是用于更改__LINE__ 和 __FILE__变量的值. 文件名是可选的. LINE 和 FILE 变量描述被读取的当前文件和行. 命令

#line 10 "main.cpp"

更改行号为10,当前文件改为"main.cpp".

new 操作

new和operator new之间的关系
A* a = new A;我们知道这里分为两步:1.分配内存,2.调用A()构造对象。事实上,分配内存这一操作就是由operator new(size_t)来完成的,如果类A重载了operator new,那么将调用A::operator new(size_t ),如果没有重载,就调用::operator new(size_t ),全局new操作符由C++默认提供。因此前面的两步也就是:1.调用operator new 2.调用构造函数。

(1)new :不能被重载,其行为总是一致的。它先调用operator new分配内存,然后调用构造函数初始化那段内存。

new 操作符的执行过程:

  1. 调用operator new分配内存 ;
  2. 调用构造函数生成类对象;
  3. 返回相应指针。

(2)operator new:要实现不同的内存分配行为,应该重载operator new,而不是new。

operator new就像operator + 一样,是可以重载的。如果类中没有重载operator new,那么调用的就是全局的::operator new来完成堆的分配。同理,operator new[]、operator delete、operator delete[]也是可以重载的。
如何限制对象只能建立在堆上或者栈上

在C++中,类的对象建立分为两种,一种是静态建立,如A a;另一种是动态建立,如A* ptr=new A;这两种方式是有区别的。

静态建立一个类对象,是由编译器为对象在栈空间中分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数。

动态建立类对象,是使用new运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数。

那么如何限制类对象只能在堆或者栈上建立呢?下面分别进行讨论。

c 浮点

浮点数主要由三部分组成,即符号、阶码与尾数,见下图是32位和64位浮点数的组成:
在这里插入图片描述
不过这里又引出一个问题,虽然用户可以定义一个精度值,那么如果为了提高精度,可以让该值无限小吗?显然也不可能,那么最小的精度是多少呢?其实为了方便浮点数的相关运算,标准库提供了一个float.h文件,并定义了一些辅助宏,比如用宏FLT_EPSILON表示单精度浮点数最高精度,以及表示双精度浮点数的宏DBL_EPSILON。

最后,给出一个规范的比较浮点数比较的函数。
在这里插入图片描述

对齐

union 变量共用内存应以最长的为准
struct:

struct B{ 
char a ; 
double b; 
int c ; 
}

一个结构体B占用1+7+8+4=20 double占用8个字节,所以B.b的起始位置必须是8的倍数
结构体后面加__attribute__ ((packed));可以禁止对齐。

sscanf

sscanf(“1234 abcd”,“%*d %s”,str);
忽略整数1234,读取abcd到str.

const

const 修饰指针变量有以下三种情况。

A: const 修饰指针指向的内容,则内容为不可变量。例如const int *p = 8;
B: const 修饰指针,则指针为不可变量。例如 int * const p = &a;
C: const 修饰指针和指针指向的内容,则指针和指针指向的内容都为不可变量。例如const int * const p = &a;

const修饰函数参数
自定义类型的参数传递,需要临时对象复制参数,对于临时对象的构造,需要调用构造函数,比较浪费时间,因此我们采取 const 外加引用传递的方法。

const 修饰成员函数:
任何不会修改数据成员的函数都应该声明为const 类型。如果在编写const 成员函数时,不慎修改了数据成员,或者调用了其它非const 成员函数,编译器将指出错误,这无疑会提高程序的健壮性。

整数

int32_t.min=-2147483648 int32_t.max=2147483647
int64_t.min=-9223372036854775808 int64_t.max=9223372036854775807
uint32_t.min=0 uint32_t.max=4294967295
uint64_t.min=0 uint64_t.max=18446744073709551615
输出64位整数linux下是

printf(“%lld/n”,a);

printf(“%llu/n”,a);

类初始化

如果一个类是这样定义的:
复制代码

Class A
{
     public:
          A(int pram1, int pram2, int pram3);
     privite:
          int a;
          int &b;
          const int c; 
}

复制代码

假如在构造函数中对三个私有变量进行赋值则通常会这样写:

A::A(int pram1, int pram2, int pram3)
{
     a=pram1;
     b=pram2;
     c=pram3;
}

但是,这样是编译不过的。因为常量和引用初始化必须赋值。所以上面的构造函数的写法只是简单的赋值,并不是初始化。

正确写法应该是:

A::A(int pram1, int pram2, int pram3):b(pram2),c(pram3)
{
     a=pram1;
}

采用初始化列表实现了对常量和引用的初始化。采用括号赋值的方法,括号赋值只能用在变量的初始化而不能用在定义之后的赋值。

凡是有引用类型的成员变量或者常量类型的变量的类,不能有缺省构造函数。默认构造函数没有对引用成员提供默认的初始化机制,也因此造成引用未初始化的编译错误。并且必须使用初始化列表进行初始化const对象、引用对象。

对于类成员,初始化列表使用拷贝初始化函数,如果没有定义就使用默认的拷贝构造函数(浅复制)

模板声明和定义分离

比如说有这样一个模板类,这是它的接口:

template <typename Node>
class TestTemplate{
public:
 
  TestTemplate(Node node):
  data(node) { }
 
  Node data;
 
  void print();
};

这是它的实现:

template <typename node>
void TestTemplate<node>::print(){
    std::cout << "TestTemplate " << data << std::endl;
}

如果把它们分别放在 .h 和 .cpp 文件中,链接器会报错,提示找不到实现。

在 .h 文件中模板类的实现下加这一句:

#include "TestTemplate.tpp"

然后把实现放在名为 TestTemplate.tpp 文件中,即可。

使用显式声明实现类模板的接口与实现的文件分离

假设上面那个类的接口与实现分别放在了 .h 和 .cpp 文件中。然后在 .cpp 文件中显式的声明要使用的模板类实例,比如:

template class TestTemplate<int>;

然后,使用 TestTemplate 也可以通过编译链接,但是只能使用已经显式声明的模板类实例。比如如果还要使用 TestTemplate,就要这样:

template class TestTemplate<int>;
template class TestTemplate<float>;

就是说只能只用已经显式声明过的模板类实例。

future promise

std::async和std::future

std::async创建一个后台线程执行传递的任务,这个任务只要是callable object均可,然后返回一个std::future。future储存一个多线程共享的状态,当调用future.get时会阻塞直到绑定的task执行完毕:
future和promise结合可以实现线程之间变量的同步

#include <iostream>       // std::cout
#include <functional>     // std::ref
#include <thread>         // std::thread
#include <future>         // std::promise, std::future

void print_int(std::future<int>& fut) {
    int x = fut.get(); // 获取共享状态的值.
    std::cout << "value: " << x << '\n'; // 打印 value: 10.
}

int main ()
{
    std::promise<int> prom; // 生成一个 std::promise<int> 对象.
    std::future<int> fut = prom.get_future(); // 和 future 关联.
    std::thread t(print_int, std::ref(fut)); // 将 future 交给另外一个线程t.
    prom.set_value(10); // 设置共享状态的值, 此处和线程t保持同步.
    t.join();
    return 0;
}

运算符重载

有两种形式
当运算符重载为普通函数或友元函数时,参数个数等于运算符操作数的个数。
当运算符重载为成员函数时,参数个数为操作数个数减一。
如果要重载<<以实现cout<<x<<y。需要使operator<<函数返回ostream对象的引用,因为cout<<x<<y等同于(cout<<x)<<y。

对于=运算符,只能用类成员函数来重载,当没有重载=符号时,当程序运行到赋值语句时,会自动寻找类中和赋值语句右值匹配的构造函数,否则就会调用=运算符函数。

->运算符的重载函数也必须是类成员函数,例如语句 p->m 被解释为 (p.operator->())->m。

memmove

void *memmove(void *str1, const void *str2, size_t n) 用于将地址str2的内容复制到str1,和memcpy的区别是允许str1和str2的地址在n个字节内有重叠。

字符串中的双引号

string a="a"
         "b"
         "c";
cout<<a;
return 0;

输出:abc

宏 do while

一般来说,如果宏展开后包括多个语句,需要用do{}while(0)包裹,主要是为了使这些语句被包含在一个代码块中,防止宏展开后破坏if-else的对应关系。

#define M() do { a(); b(); } while(0)
if (cond)
    M();
else
    c();
/* 预处理后 */

if (cond)
    do { a(); b(); } while(0);
else
    c();

::

【局部变量/函数】 与 【全局变量/函数】 的名字相同,IDE无法区分,这时候加上 :: 就可以调用到全局函数,访问到全局变量了。例如::open

参数列表和右值

Person(Person&& p)
: _name(move(p._name))
, _sex(move(p._sex))
, _age(p._age)
{}
参数列表还可以,显示的调用父类的构造函数
class animal
{
protected:       //成员变量,声明为protected或者public,这里选择protected
	int height;  //若声明为private,则不能被子类继承访问,会报错
	int weight;
public:
	animal(int height,int weight)   //带参的构造函数
	{
		this->height=height;
		this->weight=weight;
		cout<<"animal的带参构造函数被调用"<<endl;
	}
	virtual ~animal()
	{
		cout<<"animal的析构函数被调用"<<endl;
	}
};
//子类
class fish:public animal
{
public:
	fish():animal(20, 10) //显示调用父类的构造函数
	{
		cout<<"fish的构造函数被调用"<<endl;
	}
	virtual ~fish()
	{
		cout<<"fish的析构函数被调用"<<endl;
	}
};

类型自动转换

operator const char*() const是类型转换函数的定义,即该类型可以自动转换为const char*类型。至于最后一个const,那个大家都知道是对类成员的限制(不允许更改对象的状态)
虽然我不知道你的类是什么,但是我可以给你举一个简单的例子说明一下问题。比如我们现在自定一个一个整型(MyInt),它允许在需要使用C++语言中的int类型时将MyInt类型转换为int类型:
class MyInt {
public:
operator int () const;
private:
int elem;
};
MyInt::operator int () const
{
return elem;
}
就可以在需要使用int类型时使用MyInt。
还有你需要记住,C++中没有返回类型的函数有3个,构造函数、析构函数、类型转换函数。前两个是不写返回类型函数实现中也不允许出现return语句(所以不同于void)

decltype

decltype被称作类型说明符,它的作用是选择并返回操作数的数据类型

/ sum的类型就是函数f返回的类型
decltype(f()) sum = x;

大括号

作为函数返回值

std::string_view str() const {
    if (not _storage) {
        return {};
    }
    return {_storage->data() + _starting_offset, _storage->size() - _starting_offset};
}

初始化变量

Buffer buf{std::move(str)}

作为参数传入

_views.push_back({const_cast<char *>(str.data()), str.size()}

浅拷贝

如果一个类含有一个指针对象成员,并且没有定义拷贝构造函数。那么这个类的对象在赋值时会调用系统默认的拷贝构造函数,指针对象成员只是复制了指针值,这样的做法可能导致多次delete同一个指针。解决的方法是把指针对象成员用shared_ptr包裹起来。

string_view

c++ 17提供了一个新的性能工具std::string_view。它有着类似std::string的接口易于使用,但是又不拥有字符串的内存,避免了很多拷贝内存操作,性能接近原始c字符串指针。

  char text[]{"hello"};
  std::string str{text};
  std::string more{str};

  std::string_view svtext{"hello"};
  std::string_view svstr{svtext};
  std::string_view svmore{svstr};

如上代码:

第1行,"hello"在可执行文件的二进制中存在一份,在text字符串数组中存在一份
第2行,std::string拷贝了一份
第3行,std::string拷贝了一份
第5行及以后,在可执行文件的二进制中存在一份

因为std::string_view不拥有内存,避免了很多次内存数据拷贝。

其实std::string_view本身很简单,它只有两个成员变量,指向内存的指针和可以访问到的长度。它对它指向的内存是只读的,std::string_view的任何方法都不会改变执行的内存。

string &&

void fun(string &&s){
    s[1]='1';
    cout<<s<<endl;
}
int main()
{
    char a[] {'1','2'};
    fun(a);
    cout<<a<<endl;
    return 0;
}

即使是使用了右值引用,fun函数内的s和main函数里的a依然不是使用的同一块内存
输出:

11
12

make_shared

make_shared函数的主要功能是在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr;由于是通过shared_ptr管理内存,因此一种安全分配和使用动态内存的方法。
//p1指向一个值为"9999999999"的string

shared_ptr<string> p1 = make_shared<string>(10, '9');  
 
shared_ptr<string> p2 = make_shared<string>("hello");  
 
shared_ptr<string> p3 = make_shared<string>(); 

括号里的是初始化的参数。

emplace_back

vector.push_back一般是先构造临时对象再调用拷贝构造或者移动构造。但是emplace_back直接就地构造。

nullptr和NULL

#include <iostream>

void go(int num)
{
    std::cout << "go num" << std::endl;
}

void go(void *p)
{
    std::cout << "go p" << std::endl;
}

void main()
{
    void *p = NULL;

    go(p);//go p

    go(NULL);//go num

    go(nullptr);//go p

    system("pause");
}

map 遍历删除

在map中正确的遍历删除方法:

for(map<int,int>::iterator it = mapInt.begin(); it != mapInt.end();)
{
    if(it->second == 0)
    {
        mapInt.erase(it++);
    }
    else
    {
        it++;
    }

}

其实在mapInt.erase(it++)中,it++确实是作为一个完整的执行过程,it++的具体实现代码其实类似以下:

// postfix form: fetch and increment
map<int, int>::iterator operator++(int)//通过一个多余的int参数与prefix++区分
{
    map<int, int>::iterator tmp = *this; // fetch
    increment(); // increment,map内部由红黑树实现,此函数负责指向下一个有序元素的iterator
    return tmp; // return what was
}

上面代码的最终返回的值其实是tmp,tmp存储的是*this的旧值,this后来通过increment函数自增了,但是tmp的依然保持原值,最后将tmp返回赋值作为erase的参数,所以在mapInt.erase(it++)中,其实it++是作为一个整体执行完成了的,在传值给erase函数之前,it其自身其实已经+1了,不过后缀++返回的却是一个未执行+1操作的旧值,所以后面erase函数依然删除的是原it位置的值,同时该迭代器失效,然而之前it已经+1自增过了,所以不受其影响噢。

unique_lock

创建时会自动加锁,当退出作用域时会自动解锁,中途报错也会导致解锁。
手动加锁: std::unique_lockstd::mutex lck (mtx,std::defer_lock);

派生类使用基类的构造函数

struct A
{
A(int i) {}
A(double d,int i){}
A(float f,int i,const char* c){}
//…等等系列的构造函数版本号
};
struct B:A
{
using A::A;
int d{0};
};

在继承类中如果加入using,就相当于继承类使用基类的构造函数,省去了使用参数列表调用基类构造函数的麻烦。

wait_for

wait_for (unique_lock& lck, const chrono::duration<Rep,Period>& rel_time, Predicate pred); 阻塞当前线程,只有当经过rel_time,或者被其他线程notified时才能唤醒,且此时pred必须返回true。

std::atomic

is_lock_free 检查原子对象是否免锁
store 原子地以非原子对象替换原子对象的值
load 原子地获得原子对象的值
exchange 原子地替换原子对象的值并获得它先前持有的值(读-写)

compare_exchange_weak

compare_exchange_strong
原子地比较原子与非原子参数的值,若相等则进行交换,若不相等则进行加载
wait(c++20) 阻塞线程直到被提醒且原子值更改
notify_one(c++20) 提醒至少一个在原子对象上的等待中阻塞的线程
notify_all(c++20) 提醒所有在原子对象上的等待中阻塞的线程

td::ios:app 和 seekp

seekp可以设置输入时的文件偏移量,并且偏移量可以超过文件的大小。
在设置了ios::app后seekp失效,因为ios::app使得每次写入都必须以追加的形式。

自定义排序

可以使用compare函数或者重载比较运算符的方法

bool compare(A a1, A a2){
	return a1.a < a2.a;  //会产生升序排列,若改为>,则会产生降序; 
}
 
int main() {
 list<A> list_a;
 A a1(1,2), a2(4,6), a3(2,8);
 list_a.push_back(a1);
 list_a.push_back(a2);
 list_a.push_back(a3);
 
 list_a.sort(compare); //排序操作; 
 
 list<A>::iterator ite;
 ite=list_a.begin();
 for(int i=0;i<3;i++)  {cout<<ite->a<<endl; ite++;}
 
 return 0;
}

自定义set

这里只要定义一个重载<符号的结构体就能实现排序和去重, 应该set容器在判断是否相等的时候是调换left和right各测试一次,如果都返回false说明相等。

#include <iostream>
#include <set>
using namespace std;
struct song
{
    int m_id;
    int m_hot;
    song(int id,int hot)
    {

        this->m_id = id;
        this->m_hot = hot;
    }
    bool operator<(const struct song & right)const   //重载<运算符
    {
        if(this->m_id == right.m_id)     //根据id去重
            return false;
        else
        {
            if(this->m_hot != right.m_hot)
            {
                return this->m_hot > right.m_hot;      //降序
            }
            else
            {
                return this->m_id > right.m_id;     
            }
        }
    }
};
void main()
{
    std::set<song> mySet;
    song s1(10,100);
    song s2(20,200);
    song s3(20,300);
    song s4(30,200);
    mySet.insert(s1);    //插入s1
    mySet.insert(s2);    //插入s2
    mySet.insert(s3);    //s3和s2的id相同,不插入
    mySet.insert(s4);    //插入s4
    for(auto it:mySet)
    {
        std::cout<<"id:"<<it.m_id<<",hot:"<<it.m_hot<<std::endl;
    }
    std::cout<<"end"<<std::endl;
};

模板的模板参数

一个模板也可以看成一个函数,而函数的形参除了是类型之外,同样也可以是另一个模板

template<typename T,
         template<typename Elem, typename Allocator = std::allocator<Elem>> class Container = std::vector>

struct Stack
{
    void push(const T& elem)
    {
        elems.push_back(elem);
    }

    T pop()
    {
        if(empty()) throw std::out_of_range("Stack<>::pop: empty!");

        auto elem = elems.back();
        elems.pop_back();
        return elem;
    }

    bool empty() const
    {
        return elems.empty();
    }

private:
    Container<T> elems;
};

其中Stack模板的第二个形参Container同样也是一个模板函数,Container的参数可以在stack定义内传入,例如上面的 Container<T>。
stack模板类的使用方法

Stack<int, std::deque> intStack

类模板和友元

class C {
	friend class Pal<C>; //"以类C实例化"的Pal类, 为C的友元
	template <typename T> friend class Pal2; //Pal2类的所有实例化, 都为C的友元
private:
	void print() { std::cout << "class C" << std::endl; }
};

模版类不同具化类之间的转换

很多时候我们会写出下面这样的代码

template <typename T>
class A
{
public:
    A(T _a):m_a(_a){}    
    void print()
    {   
        cout<<m_a<<endl;
    }   
    T m_a;
};
int main()
{    
    A<int> aint(20);
    A<float> afloat(50);
    afloat=aint;
    return 0;
}

上面的代码看起来很好,但是却编译不过。问题出在afloat=aint这句,你可能会说,我不要这句,不这么写就完了呗。但是实际问题中,一个int赋值给一个float是显而易见正确和有意义的,所以非常有可能我们恰恰就需要这样的代码,而且它在逻辑上也说得通,这就是模版类不同具现类之间转换。

不同类之间要实现转换,很简单写一个这样的类为参数的构造函数就可以了,但是对于模版,我们不可能这样做。我们应该写一个不同模版形参的模版类的构造函数就像这样:

template <typename T>
class A
{
public:
    explicit A(T _a):m_a(_a){}
    //就像这样!!!!!!
    template<typename U>
    A(A<U>& _a):m_a(_a.m_a){}
    void print()
    {   
        cout<<m_a<<endl;
    }   
    T m_a;
};

模板递归

template<size_t DEPTH> void recur(){
    std::cout<<DEPTH<<std::endl;
    if(DEPTH==0){
        return;
    }else{
        recur<DEPTH-1>();
        return;
    }
}

直接利用条件语句对模板参数进行判断会导致递归一直进行下去(因为模板实例化是在编译阶段,所以不会运行if语句)。解决方法是用模板特化来终止递归。

template<size_t DEPTH> void recur(){
    std::cout<<DEPTH<<std::endl;
    recur<DEPTH-1>();
    return;
}
template<> void recur<0>(){
    std::cout<<0<<std::endl;
    return;
}

模板的隐式推导

关于 C++ 中模板的显示实参的匹配顺序问题

template <class T1, class T2, class T3>
T3 f( T1, T2 );

然后是函数调用:

f<double>( 3L, 4 );

这样是无法通过编译,因为显式指定优先于隐式推导,c++编译器会把double赋值给T1,所以模板应该这样定义

template <class T3, class T2, class T1>
T3 f( T1, T2 );

自定义类型的前置声明

假如类A要引用在另一个头文件中定义的类B,那么可以在类A之前先前置声明B,从而避免include B的头文件,但是A类中只能使用B的指针,因为指针大小固定。

SFINAE

Substitution failure is not an error
主要思想就是函数调用在匹配不同的函数签名时,会把模板参数用实参代替,如果编译失败不代表匹配失败,而是接着去匹配下一个签名。

 struct X {
  typedef int type;
};

struct Y {
  typedef int type2;
};

template <typename T> void foo(typename T::type);    // Foo0
template <typename T> void foo(typename T::type2);   // Foo1
template <typename T> void foo(T);                   // Foo2

void callFoo() {
   foo<X>(5);    // Foo0: Succeed, Foo1: Failed,  Foo2: Failed
   foo<Y>(10);   // Foo0: Failed,  Foo1: Succeed, Foo2: Failed
   foo<int>(15); // Foo0: Failed,  Foo1: Failed,  Foo2: Succeed
}

有的时候会有多个签名都可以匹配的情况,比如

struct ICounter {
  virtual void increase() = 0;
  virtual ~ICounter() {}
};

struct Counter: public ICounter {
   void increase() override {
      // Implements
   }
};

template <typename T>
void inc_counter(T& counterObj) {
  counterObj.increase();
}

template <typename T>
void inc_counter(T& intTypeCounter){
  ++intTypeCounter;
}

void doSomething() {
  Counter cntObj;
  uint32_t cntUI32;

  // blah blah blah
  inc_counter(cntObj);
  inc_counter(cntUI32);
}

这种情况下,我们要在签名中增加一个限定条件

struct Counter {
   void increase() {
      // Implements
   }
};

template <typename T>
void inc_counter(T& intTypeCounter, std::decay_t<decltype(++intTypeCounter)>* = nullptr) {
  ++intTypeCounter;
}

template <typename T>
void inc_counter(T& counterObj, std::decay_t<decltype(counterObj.increase())>* = nullptr) {
  counterObj.increase();
}

void doSomething() {
  Counter cntObj;
  uint32_t cntUI32;

  // blah blah blah
  inc_counter(cntObj);
  inc_counter(cntUI32);
}

即模板实参应该实现了某个特定的方法,这个思想有点类似于go语言中的接口。其中decltype获取函数的返回类型,decy_t负责去除这个类型的修饰符,比如const 或&

类static成员变量初始化

在test.h文件中:

class A

{

public:

static int a; //静态数据成员声明

}

在test.cpp文件中:

int A::a = 1; //静态数据成员的定义和初始化

类中的静态成员变量的初始化必须在类外实现!!

std::initializer_list

方便了对于STL的container的初始化
例如:之前初始化一个vector需要这样:

std::vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);

有了initializer_list后:

std::vector v = { 1, 2, 3, 4 };

原因:vector提供了这样的构造函数

template <typename T>
vector::vector(std::initializer_list<T> initList);

一个简单的反射系统

我们把反射系统所需要的信息记录在类中名为reflection的静态成员变量中,除了类名name和size 之外,如果类中有其它成员变量,那么这些成员的名字,偏移量以及各自的reflection指针需要记录在当前reflection的members数组中。
我们要做的就是在编译期间为每一个类构造一个reflection的初始化函数。
用法:

#include <vector>
#include "Reflect.h"

struct Node {
    std::string key;
    int value;
    std::vector<Node> children;

    REFLECT()       // Enable reflection for this type
};

int main() {
    // Create an object of type Node
    Node node = {"apple", 3, {{"banana", 7, {}}, {"cherry", 11, {}}}};

    // Find Node's type descriptor
    reflect::TypeDescriptor* typeDesc = reflect::TypeResolver<Node>::get();

    // Dump a description of the Node object to the console
    typeDesc->dump(&node);

    return 0;
}

// Define Node's type descriptor
REFLECT_STRUCT_BEGIN(Node)
REFLECT_STRUCT_MEMBER(key)
REFLECT_STRUCT_MEMBER(value)
REFLECT_STRUCT_MEMBER(children)
REFLECT_STRUCT_END()

其中宏REFLECT() 用来在类中插入reflection成员,后面的宏用来构造并调用reflection初始化函数,这里有一个缺点就是必须手动的输入成员变量的名字。

#define REFLECT_STRUCT_MEMBER(name) \
            {#name, offsetof(T, name), reflect::TypeResolver<decltype(T::name)>::get()},

这条宏利用offsetof拿到成员的偏移量,利用decltype(T::name)拿到成员的类型并以模板参数的形式传给TypeResolver,后者负责获取对应类型的reflection.

利用模板类TypeResolver的get方法来获取某个类的reflection.

template <typename T>
struct TypeResolver {
    static TypeDescriptor* get() {
        return DefaultResolver::get<T>();
    }
};

template <typename T>
class TypeResolver<std::vector<T>> {
public:
    static TypeDescriptor* get() {
        static TypeDescriptor_StdVector typeDesc{(T*) nullptr};
        return &typeDesc;
    }
};

首先TypeResolver的偏特化用来区分vector类型和其它类型。

struct DefaultResolver {
    template <typename T> static char func(decltype(&T::Reflection));
    template <typename T> static int func(...);
    template <typename T>
    struct IsReflected {
        enum { value = (sizeof(func<T>(nullptr)) == sizeof(char)) };
    };

    // This version is called if T has a static member named "Reflection":
    template <typename T, typename std::enable_if<IsReflected<T>::value, int>::type = 0>
    static TypeDescriptor* get() {
        return &T::Reflection;
    }

    // This version is called otherwise:
    template <typename T, typename std::enable_if<!IsReflected<T>::value, int>::type = 0>
    static TypeDescriptor* get() {
        return getPrimitiveDescriptor<T>();
    }
};

如果排除了vector的情况,随后用SFINAE来区分基础类型和自定义类型。如果调用get时,该自定义类型的reflection还没有初始化没有关系。

偏特化应用

  • 获取vector数组元素的类型
template <typename T>
struct TypeResolver {

};


template <typename T>
struct TypeResolver<std::vector<T>> {
     T a;
};

int main(){
    TypeResolver<vector<string>> T;

    cout<<typeid(T.a).name();
}
  • std::enable_if
template<bool B, class T = void>
struct enable_if {};
 
template<class T>
struct enable_if<true, T> { typedef T type; };

这个模板类只有在第一个模板参数是true的时候才会定义type, 这个性质可以用于在 SFINAE增加一个判断条件。

虚析构

把父类中析构函数定义成虚函数是为了防止delete 指向子类的父类指针时,不能删除子类成员, delete时会先调用子类析构再调用父类虚析构函数。

当父类析构函数不是virtual时,如果delete子类指针还是可以达到上述的效果,不过代价是牺牲了多态性,

windows迁移到linux

template<bool> struct Rec;
		template<>
		struct Rec<true> { using type = TopoOrderBaseSet; };
		template<>
		struct Rec<false> {
			using TVBList = typename HeadInstance::TVBList;
			using NewTopoOrderBaseSet = typename TopoSort<TVBList, TopoOrderBaseSet, ArgList, CRTP>::type;
			using type = TPushFront_t<NewTopoOrderBaseSet, THead>;
		};

以上代码在linux中会报错,因为类定义内不能进行模板特化,可以改为以下偏特化

		template<bool,class Dummy=void> struct Rec;
		template<class Dummy>
		struct Rec<true,Dummy> { using type = TopoOrderBaseSet; };
		template<class Dummy>
		struct Rec<false,Dummy> {
			using TVBList = typename HeadInstance::TVBList;
			using NewTopoOrderBaseSet = typename TopoSort<TVBList, TopoOrderBaseSet, ArgList, CRTP>::type;
			using type = TPushFront_t<NewTopoOrderBaseSet, THead>;
		};

可变参数函数模板

#include<iostream>
#include<cstdarg>
using namespace std;
 
//用来终止递归并处理包中最后一个元素
template<typename T>
void print(const T& t)
{
	cout << t;
}
 
template<typename T, typename...Args>
void print(const T& t, const Args&...rest)
{
	cout << t << " ";    //打印第一个实参
	print(rest...);     //递归调用,打印其它实参
}
 
int main()
{
	print("Harris", 2, 3.14f, "April", 42);     //输出:"Harris"  2  3.14f  "April"  42
	cout << endl;
	return 0;
}

以上代码用可变参数函数模板递归实现了打印任意数量的输入参数。

operator()

callable对象包括函数指针,重载了operator()的对象。 一个应用场景是stl中的for_each

template <typename T>
struct Print
{
    void operator()(const T& x) const
    {
        std::cout << x << " ";
    }
};

for_each(arr, arr + 5, Print<int>{});

前缀式和后缀式自增和自减

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

class Point
{
private:
	int x;
public:
	Point(int x1) :x(x1) {};
	int getx() { return this->x; };
	Point& operator++();//成员函数定义自增
    Point operator++(int n); //后缀可以返回一个const类型的值
	Point& operator--();//成员函数定义自减
	Point operator--(int n);//后缀可以返回一个const类型的值
};

Point& Point::operator++()//++obj
{
	cout << "Point& Point::operator++()    ++obj" << endl;
	x++;
	return *this;   //通过引用返回*this ,也就是返回变化之后的数值
} 
Point Point::operator++(int n)//obj++
{
	cout << "const Point Point::operator++(int n)    obj++" << endl;
	Point temp = *this;
	(this->x)++;
	return temp;   //返回原状态的拷贝
}
Point& Point::operator--()//--obj
{
	cout << "Point& Point::operator--()    --obj" << endl;
	x--;
	return *this;
	//通过引用返回*this ,也就是返回变化之后的数值
}
Point Point::operator--(int n)//obj--
{
	cout << "const Point Point::operator--(int n)    obj--" << endl;
	Point temp = *this;
	x--;
	return temp;
	// 返回原状态的拷贝
}

operator++(int n) 是后缀式自增,在调用

Point a(1);
cout << "a++" << "\t";

时,相当于隐式调用成员函数operator++(0),并且后缀式自增函数返回的是当前对象的拷贝,并且拷贝值不加1.

ptrdiff_t

ptrdiff_t 和 size_t 类似
ptrdiff_t 类型则应保证足以存放同一数组中两个指针之间的差距,它有可能是负数

realloc

(char *)realloc(p,256);
如果原指针后面有足够的空间,直接追加,如果p后面空间不够,直接在堆内存中申请一块新内存,并把p的内复制过去。
调用成功返回 v o i d ∗ void* void 指针,否则返回NULL。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++ 语法杂记 的相关文章

随机推荐