编译
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
{
public:
char *_pstr;
int _size;
explicit CxString(int size)
{
_size = size;
}
CxString(const char *p)
{
}
};
CxString string1(24);
CxString string2 = 10;
CxString string3;
CxString string4("aaaa");
CxString string5 = "bbb";
CxString string6 = 'c';
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;
}
虚继承
用于应对菱形继承情况下,派生类从多个路径继承基类的成员变量造成的命名冲突问题:
class A{
protected:
int m_a;
};
class B: virtual public A{
protected:
int m_b;
};
class C: virtual public A{
protected:
int m_c;
};
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、全局变量或函数本质上讲没有区别,函数名是指向函数二进制块开头处的指针。而全局变量是在函数外部声明的变量。函数名也在函数外,因此函数也是全局的。
#include <stdio.h>
extern int g_X ;
extern int g_Y ;
int max()
{
return (g_X > g_Y ? g_X : g_Y);
}
#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);
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
#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();
string&& name = getName();
const int& val = getVal();
int& val = getVal();
std::move
这条语句可以将左值转换为右值,可以用在函数的返回值
而右值返回值可用于类的转移构造函数
拷贝构造函数的调用时机:
- 当函数的参数为类的对象时
- 函数的返回值是类的对象
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对象。 - 对象需要通过另外一个对象继续初始化
一个转移赋值的例子:
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 tmp;
return std::move(tmp);
}
B& f2()
{
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));
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;
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;
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。
#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;
}
让我们编译并运行上面的程序,这将产生以下结果:
15 和 56 的和 = 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;
};
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 操作符的执行过程:
- 调用operator new分配内存 ;
- 调用构造函数生成类对象;
- 返回相应指针。
(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>
#include <functional>
#include <thread>
#include <future>
void print_int(std::future<int>& fut) {
int x = fut.get();
std::cout << "value: " << x << '\n';
}
int main ()
{
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(print_int, std::ref(fut));
prom.set_value(10);
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:
int height;
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(NULL);
go(nullptr);
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++的具体实现代码其实类似以下:
map<int, int>::iterator operator++(int)
{
map<int, int>::iterator tmp = *this;
increment();
return tmp;
}
上面代码的最终返回的值其实是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)
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);
mySet.insert(s2);
mySet.insert(s3);
mySet.insert(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>;
template <typename T> friend class Pal2;
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);
template <typename T> void foo(typename T::type2);
template <typename T> void foo(T);
void callFoo() {
foo<X>(5);
foo<Y>(10);
foo<int>(15);
}
有的时候会有多个签名都可以匹配的情况,比如
struct ICounter {
virtual void increase() = 0;
virtual ~ICounter() {}
};
struct Counter: public ICounter {
void increase() override {
}
};
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;
inc_counter(cntObj);
inc_counter(cntUI32);
}
这种情况下,我们要在签名中增加一个限定条件
struct Counter {
void increase() {
}
};
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;
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()
};
int main() {
Node node = {"apple", 3, {{"banana", 7, {}}, {"cherry", 11, {}}}};
reflect::TypeDescriptor* typeDesc = reflect::TypeResolver<Node>::get();
typeDesc->dump(&node);
return 0;
}
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)) };
};
template <typename T, typename std::enable_if<IsReflected<T>::value, int>::type = 0>
static TypeDescriptor* get() {
return &T::Reflection;
}
template <typename T, typename std::enable_if<!IsReflected<T>::value, int>::type = 0>
static TypeDescriptor* get() {
return getPrimitiveDescriptor<T>();
}
};
如果排除了vector的情况,随后用SFINAE来区分基础类型和自定义类型。如果调用get时,该自定义类型的reflection还没有初始化没有关系。
偏特化应用
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();
}
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);
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);
Point& operator--();
Point operator--(int n);
};
Point& Point::operator++()
{
cout << "Point& Point::operator++() ++obj" << endl;
x++;
return *this;
}
Point Point::operator++(int n)
{
cout << "const Point Point::operator++(int n) obj++" << endl;
Point temp = *this;
(this->x)++;
return temp;
}
Point& Point::operator--()
{
cout << "Point& Point::operator--() --obj" << endl;
x--;
return *this;
}
Point Point::operator--(int n)
{
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(使用前将#替换为@)