参考链接:
-在设计类的时候,一个原则就是对于不改变数据成员的成员函数都要在后面加 const,而对于改变数据成员的成员函数不能加 const。所以 const 关键字对成员函数的行为作了更加明确的限定:有 const 修饰的成员函数(指 const 放在函数参数表的后面,而不是在函数前面或者参数表内),只能读取数据成员,不能改变数据成员;没有 const 修饰的成员函数,对数据成员则是可读可写的。
除此之外,在类的成员函数后面加 const 还有什么好处呢?
“获得能力:可以操作常量对象”,其实应该是常量(即 const)对象可以调用 const 成员函数,而不能调用非const修饰的函数。
对于const成员函数,"不能修改类的数据成员,不能在函数中调用其他不是const的函数",这是由const的属性决定的 。
static静态成员变量不能在类的内部初始化。在类的内部只是声明,定义必须在类定义体的外部,通常在类的实现文件中初始化。
由于声明为static的变量只被初始化一次,因为它们在单独的静态存储中分配了空间,因此类中的静态变量由对象共享。对于不同的对象,不能有相同静态变量的多个副本。也是因为这个原因,静态变量不能使用构造函数初始化。
类中的静态变量应由用户使用类外的类名和范围解析运算符显式初始化
静态对象的范围是贯穿程序的生命周期
一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。
在类的非静态成员函数中返回类对象本身的时候,直接使用 return *this。
当参数与成员变量名相同时,如this->n = n (不能写成n = n)。
this在成员函数的开始执行前构造,在成员的执行结束后清除。
this类型为const A* const。A为类。
内联能提高函数效率,但并不是所有的函数都定义成内联函数!内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。
虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
内联是在编译器建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
sizeof: 普通继承,派生类继承了所有基类的函数与成员,要按照字节对齐来计算大小
sizeof:虚函数继承,不管是单继承还是多继承,都是继承了基类的vptr。(32位操作系统4字节,64位操作系统 8字节)!
sizeof: 静态变量不影响类的大小
sizeof:对于包含虚函数的类,不管有多少个虚函数,只有一个虚指针,vptr的大小。
sizeof: 派生类虚继承多个虚函数,会继承所有虚函数的vptr。
抽象类中:在成员函数内可以调用纯虚函数,在构造函数/析构函数内部不能使用纯虚函数。
如果一个类从抽象类派生而来,它必须实现了基类中的所有纯虚函数,才能成为非抽象类。
抽象类至少包含一个纯虚函数
不能创建抽象类的对象
抽象类的指针和引用 指向 由抽象类派生出来的类的对象
派生类没有实现纯虚函数,那么派生类也会变为抽象类,不能创建抽象类的对象
虚函数的调用取决于指向或者引用的对象的类型,而不是指针或者引用自身的类型。
默认参数是静态绑定的,虚函数是动态绑定的。 默认参数的使用需要看指针或者引用本身的类型,而不是对象的类型。
静态函数不可以声明为虚函数,同时也不能被const 和 volatile关键字修饰
为什么构造函数不可以为虚函数?
解:尽管虚函数表vtable是在编译阶段就已经建立的,但指向虚函数表的指针vptr是在运行阶段实例化对象时才产生的。 如果类含有虚函数,编译器会在构造函数中添加代码来创建vptr。 问题来了,如果构造函数是虚的,那么它需要vptr来访问vtable,可这个时候vptr还没产生。 因此,构造函数不可以为虚函数。虚函数可以被私有化,但有一些细节需要注意。
1 基类指针指向继承类对象,则调用继承类对象的函数;
2 int main()必须声明为Base类的友元,否则编译失败。 编译器报错: ptr无法访问私有函数。
3 当然,把基类声明为public, 继承类为private,该问题就不存在了。volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)
const 可以是 volatile (如只读的状态寄存器)
指针可以是 volatile
断言,是宏,而非函数。assert 宏的原型定义在 (C)、(C++)中,其作用是如果它的条件返回错误,则终止程序执行。可以通过定义 NDEBUG 来关闭 assert,但是需要在源代码的开头,include 之前。
断言主要用于检查逻辑上不可能的情况。
它们可用于检查代码在开始运行之前所期望的状态,或者在运行完成后检查状态。与正常的错误处理不同,断言通常在运行时被禁用。
忽略断言:在代码开头加上:#define NDEBUG // 加上这行,则 assert 不可用
extern "C"全部都放在于cpp程序相关文件或其头文件中。
在C中struct只单纯的用作数据的复合类型,也就是说,在结构体声明中只能将数据成员放在里面,而不能将函数放在里面。
在C结构体声明中不能使用C++访问修饰符,如:public、protected、private 而在C++中可以使用。
在C中定义结构体变量,如果使用了下面定义必须加struct。
C的结构体不能继承(没有这一概念)。
若结构体的名字与函数名相同,可以正常运行且正常的调用!例如:可以定义与 struct Base 不冲突的 void Base() {}。
与C对比如下:
1.C++结构体中不仅可以定义数据,还可以定义函数。
2.C++结构体中可以使用访问修饰符,如:public、protected、private 。
3.C++结构体使用可以直接使用不带struct。
4.C++继承若结构体的名字与函数名相同,可以正常运行且正常的调用!但是定义结构体变量时候只用带struct的!union
1.默认访问控制符为 public
2.可以含有构造函数、析构函数
3.不能含有引用类型的成员
4.不能继承自其他类,不能作为基类
5.不能含有虚函数
6.匿名 union 在定义所在作用域可直接访问 union 成员
7.匿名 union 不能包含 protected 成员或 private 成员
8.全局匿名联合必须是静态(static)的-
.C实现多态
- 封装
C语言中是没有class类这个概念的,但是有struct结构体,我们可以考虑使用struct来模拟;
使用函数指针把属性与方法封装到结构体中。 - 继承
结构体嵌套 - 多态、
类与子类方法的函数指针不同
在C语言的结构体内部是没有成员函数的,如果实现这个父结构体和子结构体共有的函数呢?我们可以考虑使用函数指针来模拟。但是这样处理存在一个缺陷就是:父子各自的函数指针之间指向的不是类似C++中维护的虚函数表而是一块物理内存,如果模拟的函数过多的话就会不容易维护了。
模拟多态,必须保持函数指针变量对齐(在内容上完全一致,而且变量对齐上也完全一致)。否则父类指针指向子类对象,运行崩溃!
#include <stdio.h>
// 重定义一个函数指针类型
typedef void (*pf) ();
/**
父类
*/
typedef struct _A
{
pf _f;
}A;
/**
子类
*/
typedef struct _B
{
A _b; // 在子类中定义一个基类的对象即可实现对父类的继承。
}B;
void FunA()
{
printf("%s\n","Base A::fun()");
}
void FunB()
{
printf("%s\n","Derived B::fun()");
}
int main()
{
A a;
B b;
a._f = FunA;
b._b._f = FunB;
A *pa = &a;
pa->_f();
pa = (A *)&b; // 让父类指针指向子类的对象,由于类型不匹配所以要进行强转
pa->_f();
return 0;
}
- explicit 修饰构造函数时,可以防止隐式转换和复制初始化
- explicit 修饰转换函数时,可以防止隐式转换,但按语境转换除外
#include <iostream>
using namespace std;
struct A
{
A(int) { }
operator bool() const { return true; }
};
struct B
{
explicit B(int) {}
explicit operator bool() const { return true; }
};
void doA(A a) {}
void doB(B b) {}
int main()
{
A a1(1); // OK:直接初始化
A a2 = 1; // OK:复制初始化
A a3{ 1 }; // OK:直接列表初始化
A a4 = { 1 }; // OK:复制列表初始化
A a5 = (A)1; // OK:允许 static_cast 的显式转换
doA(1); // OK:允许从 int 到 A 的隐式转换
if (a1); // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
bool a6(a1); // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
bool a7 = a1; // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
bool a8 = static_cast<bool>(a1); // OK :static_cast 进行直接初始化
B b1(1); // OK:直接初始化
// B b2 = 1; // 错误:被 explicit 修饰构造函数的对象不可以复制初始化
B b3{ 1 }; // OK:直接列表初始化
// B b4 = { 1 }; // 错误:被 explicit 修饰构造函数的对象不可以复制列表初始化
B b5 = (B)1; // OK:允许 static_cast 的显式转换
// doB(1); // 错误:被 explicit 修饰构造函数的对象不可以从 int 到 B 的隐式转换
if (b1); // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换
bool b6(b1); // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换
// bool b7 = b1; // 错误:被 explicit 修饰转换函数 B::operator bool() 的对象不可以隐式转换
bool b8 = static_cast<bool>(b1); // OK:static_cast 进行直接初始化
return 0;
}
- 友元提供了一种 普通函数或者类成员函数 访问另一个类中的私有或保护成员 的机制。也就是说有两种形式的友元:
(1)友元函数:普通函数对一个访问某个类中的私有或保护成员。
(2)友元类:类A中的成员函数访问类B中的私有或保护成员
优点:提高了程序的运行效率。
缺点:破坏了类的封装性和数据的透明性。
总结: - 能访问私有成员 - 破坏封装性 - 友元关系不可传递 - 友元关系的单向性 - 友元声明的形式及数量不受限制
在类声明的任何区域中声明,而定义则在类的外部。
friend <类型><友元函数名>(<参数表>);
注意,友元函数只是一个普通函数,并不是该类的类成员函数,它可以在任何地方调用,友元函数中通过对象名来访问该类的私有或保护成员。
#include <iostream>
using namespace std;
class A
{
public:
A(int _a):a(_a){};
friend int geta(A &ca); ///< 友元函数
private:
int a;
};
int geta(A &ca)
{
return ca.a;
}
int main()
{
A a(3);
cout<<geta(a)<<endl;
return 0;
}
友元类的声明在该类的声明中,而实现在该类外。
friend class <友元类名>;
类B是类A的友元,那么类B可以直接访问A的私有成员。
#include <iostream>
using namespace std;
class A
{
public:
A(int _a):a(_a){};
friend class B;
private:
int a;
};
class B
{
public:
int getb(A ca) {
return ca.a;
};
};
int main()
{
A a(3);
B b;
cout<<b.getb(a)<<endl;
return 0;
}
友元关系没有继承性 假如类B是类A的友元,类C继承于类A,那么友元类B是没办法直接访问类C的私有或保护成员。
友元关系没有传递性 假如类B是类A的友元,类C是类B的友元,那么友元类C是没办法直接访问类A的私有或保护成员,也就是不存在“友元的友元”这种关系。
- using
#include <iostream>
#define isNs1 1
//#define isGlobal 2
using namespace std;
void func()
{
cout<<"::func"<<endl;
}
namespace ns1 {
void func()
{
cout<<"ns1::func"<<endl;
}
}
namespace ns2 {
#ifdef isNs1
using ns1::func; /// ns1中的函数
#elif isGlobal
using ::func; /// 全局中的函数
#else
void func()
{
cout<<"other::func"<<endl;
}
#endif
}
int main()
{
/**
* 这就是为什么在c++中使用了cmath而不是math.h头文件
*/
ns2::func(); // 会根据当前环境定义宏的不同来调用不同命名空间下的func()函数
return 0;
}
class Base{
public:
std::size_t size() const { return n; }
protected:
std::size_t n;
};
class Derived : private Base {
public:
using Base::size;
protected:
using Base::n;
};
在继承过程中,派生类可以覆盖重载函数的0个或多个实例,一旦定义了一个重载版本,那么其他的重载版本都会变为不可见。
如果对于基类的重载函数,我们需要在派生类中修改一个,又要让其他的保持可见,必须要重载所有版本,这样十分的繁琐。
#include <iostream>
using namespace std;
class Base{
public:
void f(){ cout<<"f()"<<endl;
}
void f(int n){
cout<<"Base::f(int)"<<endl;
}
};
class Derived : private Base {
public:
using Base::f;
void f(int n){
cout<<"Derived::f(int)"<<endl;
}
};
int main()
{
Base b;
Derived d;
d.f();
d.f(1);
return 0;
}
如上代码中,在派生类中使用using声明语句指定一个名字而不指定形参列表,所以一条基类成员函数的using声明语句就可以把该函数的所有重载实例添加到派生类的作用域中。此时,派生类只需要定义其特有的函数就行了,而无需为继承而来的其他函数重新定义。
C中常用typedef A B这样的语法,将B定义为A类型,也就是给A类型一个别名B
对应typedef A B,使用using B=A可以进行同样的操作。
typedef vector<int> V1;
using V2 = vector<int>;
全局作用域符(::name):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间
类作用域符(class::name):用于表示指定类型的作用域范围是具体某个类的
命名空间作用域符(namespace::name):用于表示指定类型的作用域范围是具体某个命名空间的
-
enum:
传统问题:- 作用域不受限,会容易引起命名冲突。例如下面无法编译通过的:
#include <iostream>
using namespace std;
enum Color {RED,BLUE};
enum Feeling {EXCITED,BLUE};
int main()
{
return 0;
}
- 会隐式转换为int
- 用来表征枚举变量的实际类型不能明确指定,从而无法支持枚举类型的前向声明。
解决作用域不受限带来的命名冲突问题的一个简单方法是,给枚举变量命名时加前缀,如上面例子改成 COLOR_BLUE 以及 FEELING_BLUE。
namespace Color
{
enum Type
{
RED=15,
YELLOW,
BLUE
};
};
这样之后就可以用 Color::Type c = Color::RED; 来定义新的枚举变量了。如果 using namespace Color 后,前缀还可以省去,使得代码简化。不过,因为命名空间是可以随后被扩充内容的,所以它提供的作用域封闭性不高。在大项目中,还是有可能不同人给不同的东西起同样的枚举类型名。
更“有效”的办法是用一个类或结构体来限定其作用域,例如:定义新变量的方法和上面命名空间的相同。不过这样就不用担心类在别处被修改内容。这里用结构体而非类,一是因为本身希望这些常量可以公开访问,二是因为它只包含数据没有成员函数。
struct Color1
{
enum Type
{
RED=102,
YELLOW,
BLUE
};
};
C++11 标准中引入了“枚举类”(enum class),可以较好地解决上述问题。
- 新的enum的作用域不在是全局的
- 不能隐式转换成其他类型
/**
* @brief C++11的枚举类
* 下面等价于enum class Color2:int
*/
enum class Color2
{
RED=2,
YELLOW,
BLUE
};
r2 c2 = Color2::RED;
cout << static_cast<int>(c2) << endl; //必须转!
- 可以指定用特定的类型来存储enum
enum class Color3:char; // 前向声明
// 定义
enum class Color3:char
{
RED='r',
BLUE
};
char c3 = static_cast<char>(Color3::RED);
class Person{
public:
typedef enum {
BOY = 0,
GIRL
}SexType;
};
//访问的时候通过,Person::BOY或者Person::GIRL来进行访问。
枚举常量不会占用对象的存储空间,它们在编译时被全部求值。
枚举常量的缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点。
- 这里的括号是必不可少的,decltype的作用是“查询表达式的类型”,因此,上面语句的效果是,返回 expression 表达式的类型。注意,decltype 仅仅“查询”表达式的类型,并不会对表达式进行“求值”。
decltype (expression)
int i = 4;
decltype(i) a; //推导结果为int。a的类型为int。
using size_t = decltype(sizeof(0));//sizeof(a)的返回值为size_t类型
using ptrdiff_t = decltype((int*)0 - (int*)0);
using nullptr_t = decltype(nullptr);
vector<int >vec;
typedef decltype(vec.begin()) vectype;
for (vectype i = vec.begin; i != vec.end(); i++)
{
//...
}
//在C++中,我们有时候会遇上一些匿名类型,如:
struct
{
int d ;
doubel b;
}anon_s;
//而借助decltype,我们可以重新使用这个匿名的结构体:
decltype(anon_s) as ;//定义了一个上面匿名的结构体
泛型编程中结合auto,用于追踪函数的返回值类型
这也是decltype最大的用途了。
template <typename T>
auto multiply(T x, T y)->decltype(x*y)
{
return x*y;
}
对于decltype(e)而言,其判别结果受以下条件的影响:
如果e是一个没有带括号的标记符表达式或者类成员访问表达式,那么的decltype(e)就是e所命名的实体的类型。此外,如果e是一个被重载的函数,则会导致编译错误。 否则 ,假设e的类型是T,如果e是一个将亡值,那么decltype(e)为T&& 否则,假设e的类型是T,如果e是一个左值,那么decltype(e)为T&。 否则,假设e的类型是T,则decltype(e)为T。
标记符指的是除去关键字、字面量等编译器需要使用的标记之外的程序员自己定义的标记,而单个标记符对应的表达式即为标记符表达式。例如:
int arr[4]
则arr为一个标记符表达式,而arr[3]+0不是。
int i = 4;
int arr[5] = { 0 };
int *ptr = arr;
struct S{ double d; }s ;
void Overloaded(int);
void Overloaded(char);//重载的函数
int && RvalRef();
const bool Func(int);
//规则一:推导为其类型
decltype (arr) var1; //int 标记符表达式
decltype (ptr) var2;//int * 标记符表达式
decltype(s.d) var3;//doubel 成员访问表达式
//decltype(Overloaded) var4;//重载函数。编译错误。
//规则二:将亡值。推导为类型的右值引用。
decltype (RvalRef()) var5 = 1;
//规则三:左值,推导为类型的引用。
decltype ((i))var6 = i; //int&
decltype (true ? i : i) var7 = i; //int& 条件表达式返回左值。
decltype (++i) var8 = i; //int& ++i返回i的左值。
decltype(arr[5]) var9 = i;//int&. []操作返回左值
decltype(*ptr)var10 = i;//int& *操作返回左值
decltype("hello")var11 = "hello"; //const char(&)[9] 字符串字面常量为左值,且为const左值。
//规则四:以上都不是,则推导为本类型
decltype(1) var12;//const int
decltype(Func(1)) var13=true;//const bool
decltype(i++) var14 = i;//int i++返回右值
-
右值引用可实现转移语义(Move Sementics)和精确传递(Perfect Forwarding),它的主要目的有两个方面:
1.消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
2.能够更简洁明确地定义泛型函数。
引用折叠
-
X& &
、X& &&
、X&& &
可折叠成X&
-
X&& &&
可折叠成X&&
C++的引用在减少了程序员自由度的同时提升了内存操作的安全性和语义的优美性。
private 是完全私有的,只有当前类中的成员能访问到.
protected 是受保护的,只有当前类的成员与继承该类的类才能访问.
- 在使用new的时候做了两件事:
1、调用operator new分配空间
2、调用构造函数初始化对象
- 在使用delete的时候也做了两件事:
1、调用析构函数清理对象
2、调用operator delete函数释放空间
- 在使用new[N]的时候也做了两件事:
1、调用operator new分配空间
2、调用N次构造函数初始化N个对象
- 在使用delete[]的时候也做了两件事:
1、调用N次析构函数清理N个对象
2、调用operator delete函数释放空间
-
1.1 字符串化操作符(#)
在一个宏中的参数前面使用一个#,预处理器会把这个参数转换为一个字符数组,换言之就是:#是“字符串化”的意思,出现在宏定义中的#是把跟在后面的参数转换成一个字符串。
-
1.2 符号连接操作符(##)
“##”是一种分隔连接方式,它的作用是先分隔,然后进行强制连接。将宏定义的多个形参转换成一个实际参数名。
注意事项:
(1)当用##连接形参时,##前后的空格可有可无。
(2)连接后的实际参数名,必须为实际存在的参数名或是编译器已知的宏定义。
(3)如果##后的参数本身也是一个宏的话,##会阻止这个宏的展开。
-
1.3 续行操作符(\
当定义的宏不能用一行表达完整时,可以用”\”表示下一行继续此宏的定义。
注意 \ 前留空格。
template<typename ...Args>
class A {
private:
int size = 0; // c++11 支持类内初始化
public:
A() {
size = sizeof...(Args);
cout << size << endl;
}
};
A<int, string, vector<int>> a; // 类型任意
// Tuple就是利用这个特性(变长参数模板)
tuple<int, string> t = make_tuple(1, "hha");
单例模式
//简单实现,只适用于单线程下。
//懒汉 线程不安全
class singleton {
private:
singleton() {}
static singleton *p;
public:
static singleton *instance();
};
singleton *singleton::p = nullptr;
singleton* singleton::instance() {
if (p == nullptr)
p = new singleton();
return p;
}
//饿汉 这个是线程安全的
class singleton {
private:
singleton() {}
static singleton *p;
public:
static singleton *instance();
};
singleton *singleton::p = new singleton();
singleton* singleton::instance() {
return p;
}
//多线程下单例模式
class singleton {
private:
singleton() {}
static singleton *p;
static mutex lock_;
public:
static singleton *instance();
};
singleton *singleton::p = nullptr;
singleton* singleton::instance() {
lock_guard<mutex> guard(lock_);
if (p == nullptr)
p = new singleton();
return p;
}
//双重检查锁+自动回收
class singleton {
private:
singleton() {}
static singleton *p;
static mutex lock_;
public:
singleton *instance();
// 实现一个内嵌垃圾回收类
class CGarbo
{
public:
~CGarbo()
{
if(singleton::p)
delete singleton::p;
}
};
static CGarbo Garbo; // 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
};
singleton *singleton::p = nullptr;
singleton::CGarbo Garbo;
singleton* singleton::instance() {
if (p == nullptr) {
lock_guard<mutex> guard(lock_);
if (p == nullptr)
p = new singleton();
}
return p;
}
5.memory barrier指令
DCLP问题在C++11中,这个问题得到了解决。
因为新的C++11规定了新的内存模型,保证了执行上述3个步骤的时候不会发生线程切换,相当这个初始化过程是“原子性”的的操作,DCL又可以正确使用了,不过在C++11下却有更简洁的多线程singleton写法了,这个留在后面再介绍。
C++11之前解决方法是barrier指令。要使其正确执行的话,就得在步骤2、3直接加上一道memory barrier。强迫CPU执行的时候按照1、2、3的步骤来运行。
第一种实现:
基于operator new+placement new,遵循1,2,3执行顺序依次编写代码。
// method 1 operator new + placement new
singleton *instance() {
if (p == nullptr) {
lock_guard<mutex> guard(lock_);
if (p == nullptr) {
singleton *tmp = static_cast<singleton *>(operator new(sizeof(singleton)));
new(p)singleton();
p = tmp;
}
}
return p;
}
**第二种实现:**
基于直接嵌入ASM汇编指令mfence,uninx的barrier宏也是通过该指令实现的。
#define barrier() __asm__ volatile ("lwsync")
singleton *singleton::instance() {
if (p == nullptr) {
lock_guard<mutex> guard(lock_);
barrier();
if (p == nullptr) {
p = new singleton();
}
}
return p;
}
通常情况下是调用cpu提供的一条指令,这条指令的作用是会阻止cpu将该指令之前的指令交换到该指令之后,这条指令也通常被叫做barrier。 上面代码中的asm表示这个是一条汇编指令,volatile是可选的,如果用了它,则表示向编译器声明不允许对该汇编指令进行优化。lwsync是POWERPC提供的barrier指令。
- Scott Meyer在《Effective C++》中提出了一种简洁的singleton写法
singleton *singleton::instance() {
static singleton p;
return &p;
}
mutex singleton::lock_;
atomic<singleton *> singleton::p;
/*
* std::atomic_thread_fence(std::memory_order_acquire);
* std::atomic_thread_fence(std::memory_order_release);
* 这两句话可以保证他们之间的语句不会发生乱序执行。
*/
singleton *singleton::instance() {
singleton *tmp = p.load(memory_order_relaxed);
atomic_thread_fence(memory_order_acquire);
if (tmp == nullptr) {
lock_guard<mutex> guard(lock_);
tmp = p.load(memory_order_relaxed);
if (tmp == nullptr) {
tmp = new singleton();
atomic_thread_fence(memory_order_release);
p.store(tmp, memory_order_relaxed);
}
}
return p;
}
值得注意的是,上述代码使用两个比较关键的术语,获得与释放:
- 获得是一个对内存的读操作,当前线程的任何后面的读写操作都不允许重排到这个操作的前面去。
- 释放是一个对内存的写操作,当前线程的任何前面的读写操作都不允许重排到这个操作的后面去。
如果是在unix平台的话,除了使用atomic operation外,在不适用C++11的情况下,还可以通过pthread_once来实现Singleton。
原型如下:
int pthread_once(pthread_once_t once_control, void (init_routine) (void));
class singleton {
private:
singleton(); //私有构造函数,不允许使用者自己生成对象
singleton(const singleton &other);
//要写成静态方法的原因:类成员函数隐含传递this指针(第一个参数)
static void init() {
p = new singleton();
}
static pthread_once_t ponce_;
static singleton *p; //静态成员变量
public:
singleton *instance() {
// init函数只会执行一次
pthread_once(&ponce_, &singleton::init);
return p;
}
};
- C++ tr1全称Technical Report 1,是针对C++标准库的第一次扩展。即将到来的下一个版本的C++标准c++0x会包括它,以及一些语言本身的扩充。tr1包括大家期待已久的smart pointer,正则表达式以及其他一些支持范型编程的内容。草案阶段,新增的类和模板的名字空间是std::tr1。
#include <tr1/array>
std::tr1::array<int ,10> a;//第一个类型,第二个大小
-
deque与vector最大的差异就是:
1.deque允许于常数时间内对头端进行插入或删除元素;
2.deque是分段连续线性空间,随时可以增加一段新的空间;
-
用户看起来deque使用的是连续空间,实际上是分段连续线性空间。为了管理分段空间deque容器引入了map,称之为中控器,map是一块连续的空间,其中每个元素是指向缓冲区的指针,缓冲区才是deque存储数据的主体。
在上图中,buffer称为缓冲区,显示map size的一段连续空间就是中控器。
中控器包含了map size,指向buffer的指针,deque的开始迭代器与结尾迭代器。
_Tp **_M_map;
size_t _M_map_size;
iterator _M_start;
iterator _M_finish;
deque是使用基类_Deque_base来完成内存管理与中控器管理
在stack的源码中我们关注两点: - 默认_Sequence为deque - 内部函数实现是调用_Sequence对应容器的函数。
对于stack来说,底层容器可以是vector、deque、list,但不可以是map、set。 由于编译器不会做全面性检查,当调用函数不存在的时候,就编译不通过,所以对于像set虽然不能作为底层容器,但如果具有某些函数,调用仍然是成功的,直到调用的函数不存在。
在queue的源码中我们关注两点: - 默认_Sequence为deque - 内部函数实现是调用_Sequence对应容器的函数。
优先队列则是使用vector作为默认容器。
对于queue底层容器可以是deque,也可以是list,但不能是vector,map,set,使用默认的deque效率在插入方面比其他容器作为底层要快!
对于优先队列来说,测试结果发现,采用deque要比默认的vector插入速度快! 底层支持vector、deque容器,但不支持list、map、set。
stack、queue、priority_queue不被称为容器, 把它称为容器配接器。
vector的数据安排以及操作方式,与array非常相似。两者的唯一差别在于空间的运用的灵活性,array是静态的,一旦配置了就不能改变,而 vector是动态空间,随着元素的加入,它的内部机制会自行扩充空间以容纳新元素。
-
类作用域
在类外部访问类中的名称时,可以使用类作用域操作符,形如MyClass::name
的调用通常存在三种:静态数据成员、静态成员函数和嵌套类型:
struct MyClass {
static int A; //静态成员
static int B(){cout<<"B()"<<endl; return 100;} //静态函数
typedef int C; //嵌套类型
struct A1 { //嵌套类型
static int s;
};
};
一种能够顺序访问容器中每个元素的方法,使用该方法不能暴露容器内部的表达方式。而类型萃取技术就是为了要解决和 iterator 有关的问题的。
总结:通过定义内嵌类型,我们获得了知晓 iterator 所指元素类型的方法,通过 traits 技法,我们将函数模板对于原生指针和自定义 iterator 的定义都统一起来,我们使用 traits 技法主要是为了解决原生指针和自定义 iterator 之间的不同所造成的代码冗余,这就是 traits 技法的妙处所在。
因为空类同样可以被实例化,每个实例在内存中都有一个独一无二的地址,为了达到这个目的,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存得到了独一无二的地址.所以上述大小为1.
两个不同对象的地址不同。
基类为空,通过继承方式来获得基类的功能,并没有产生额外大小的优化称之为EBO(空基类优化)。
第一种方式的内存管理:嵌入一个内存管理类
template<class T, class Allocator>
class MyContainerNotEBO {
T *data_ = nullptr;
std::size_t capacity_;
Allocator allocator_; // 嵌入一个MyAllocator
public:
MyContainerNotEBO(std::size_t capacity)
: capacity_(capacity), allocator_(), data_(nullptr) {
std::cout << "alloc malloc" << std::endl;
data_ = reinterpret_cast<T *>(allocator_.allocate(capacity * sizeof(T))); // 分配内存
}
~MyContainerNotEBO() {
std::cout << "MyContainerNotEBO free malloc" << std::endl;
allocator_.deallocate(data_);
}
};
- 第二种方式:采用空基类优化,继承来获得内存管理功能
template<class T, class Allocator>
class MyContainerEBO
: public Allocator { // 继承一个EBO
T *data_ = nullptr;
std::size_t capacity_;
public:
MyContainerEBO(std::size_t capacity)
: capacity_(capacity), data_(nullptr) {
std::cout << "alloc malloc" << std::endl;
data_ = reinterpret_cast<T *>(this->allocate(capacity * sizeof(T)));
}
~MyContainerEBO() {
std::cout << "MyContainerEBO free malloc" << std::endl;
this->deallocate(data_);
}
};
int main() {
MyContainerNotEBO<int, MyAllocator> notEbo = MyContainerNotEBO<int, MyAllocator>(0);
std::cout << "Using Not EBO Test sizeof is " << sizeof(notEbo) << std::endl;
MyContainerEBO<int, MyAllocator> ebo = MyContainerEBO<int, MyAllocator>(0);
std::cout << "Using EBO Test sizeof is " << sizeof(ebo) << std::endl;
return 0;
}
//结果
alloc malloc
Using Not EBO Test sizeof is 24
alloc malloc
Using EBO Test sizeof is 16
MyContainerEBO free malloc
MyContainerNotEBO free malloc
- 采用EBO的设计确实比嵌入设计好很多。
❑ 二分查找的速度比简单查找快得多。
❑ O(log n)比O(n)快。需要搜索的元素越多,前者比后者就快得越多。
❑ 算法运行时间并不以秒为单位。
❑ 算法运行时间是从其增速的角度度量的。
❑ 算法运行时间用大O表示法表示。
set/multiset以rb_tree为底层结构,因此有元素自动排序特性。排序的依据是key,而set/multiset元素的value和key合二为一:value就是key。
第一个问题:key是value,value也是key。
第二个问题:无法使用迭代器改变元素值。
第三个问题:插入是唯一的key。
cout<<"flag: "<<itree._M_insert_unique(5).second<<endl; // 学习返回值
typedef pair<int ,bool> _Res; // 也来用一下typedef后的pair
cout<<_Res(1,true).first<<endl; // 直接包裹
_Res r=make_pair(2,false); // 定义新对象
cout<<r.first<<endl; // 输出结果
- map的key为key,value为key+data,与set是不同的,set是key就是value,value就是key。
- map的key不可修改,map与multimap的插入调用函数不同,影响了其key是否对应value。
- initializer_list使用
- map有[]操作符,而multimap没有[]操作符。
insert的几种方法:
(1) 插入 pair
std::pair<iterator, bool> insert(const value_type& __x)
{ return _M_t._M_insert_unique(__x); }
map里面
(2) 在指定位置,插入pair
iterator insert(iterator __position, const value_type& __x)
{ return _M_t._M_insert_equal_(__position, __x); }
(3) 从一个范围进行插入
template<typename _InputIterator>
void
insert(_InputIterator __first, _InputIterator __last)
{ _M_t._M_insert_equal(__first, __last); }
(4)从list中插入
void
insert(initializer_list<value_type> __l)
{ this->insert(__l.begin(), __l.end()); }
针对最后一个insert,里面有个initializer_list,举个例子大家就知道了。
- 结论1:undered_map与undered_set不允许key重复,而带multi的则允许key重复;
- 结论2:undered_map与undered_multimap采用的迭代器是iterator,而undered_set与undered_multiset采用的迭代器是const_iterator。
- 结论3:undered_map与undered_multimap的key是key,value是key+value;而undered_set与undered_multiset的key是Value,Value也是Key。
五种创建线程的方式
- 函数指针
- Lambda函数吧
- Functor(仿函数)
- 非静态成员函数
- 静态成员函数
2.1 函数指针
// 1.函数指针
void fun(int x) {
while (x-- > 0) {
cout << x << endl;
}
}
// 调用
std::thread t1(fun, 10);
t1.join();
2.2 Lambda函数
// 注意:如果我们创建多线程 并不会保证哪一个先开始
int main() {
// 2.Lambda函数
auto fun = [](int x) {
while (x-- > 0) {
cout << x << endl;
}
};
// std::1.thread t1(fun, 10);
// 也可以写成下面:
std::thread t1_1([](int x) {
while (x-- > 0) {
cout << x << endl;
}
}, 11);
// std::1.thread t2(fun, 10);
// t1.join();
t1_1.join();
// t2.join();
return 0;
}
2.3 仿函数
// 3.functor (Funciton Object)
class Base {
public:
void operator()(int x) {
while (x-- > 0) {
cout << x << endl;
}
}
};
// 调用
thread t(Base(), 10);
t.join();
2.4 非静态成员函数
// 4.Non-static member function
class Base {
public:
void fun(int x) {
while (x-- > 0) {
cout << x << endl;
}
}
};
// 调用
thread t(&Base::fun,&b, 10);
t.join();
2.5 静态成员函数
// 4.Non-static member function
class Base {
public:
static void fun(int x) {
while (x-- > 0) {
cout << x << endl;
}
}
};
// 调用
thread t(&Base::fun, 10);
t.join();
join
- 一旦线程开始,我们要想等待线程完成,需要在该对象上调用join()
- 双重join将导致程序终止
- 在join之前我们应该检查显示是否可以被join,通过使用joinable()
void run(int count) {
while (count-- > 0) {
cout << count << endl;
}
std::this_thread::sleep_for(chrono::seconds(3));
}
int main() {
thread t1(run, 10);
cout << "main()" << endl;
t1.join();
if (t1.joinable()) {
t1.join();
}
cout << "main() after" << endl;
return 0;
}
detach
- 这用于从父线程分离新创建的线程
- 在分离线程之前,请务必检查它是否可以joinable,否则可能会导致两次分离,并且双重detach()将导致程序终止
- 如果我们有分离的线程并且main函数正在返回,那么分离的线程执行将被挂起
void run(int count) {
while (count-- > 0) {
cout << count << endl;
}
std::this_thread::sleep_for(chrono::seconds(3));
}
int main() {
thread t1(run, 10);
cout << "main()" << endl;
t1.detach();
if(t1.joinable())
t1.detach();
cout << "main() after" << endl;
return 0;
-
增加变量(i ++)的过程分三个步骤:
1.将内存内容复制到CPU寄存器。 load 2.在CPU中增加该值。 increment 3.将新值存储在内存中。 store
- 如果只能通过一个线程访问该内存位置(例如下面的变量i),则不会出现争用情况,也没有与i关联的临界区。 但是sum变量是一个全局变量,可以通过两个线程进行访问。 两个线程可能会尝试同时增加变量。
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
int sum = 0; //shared
mutex m;
void *countgold() {
int i; //local to each thread
for (i = 0; i < 10000000; i++) {
sum += 1;
}
return NULL;
}
int main() {
thread t1(countgold);
thread t2(countgold);
//Wait for both threads to finish
t1.join();
t2.join();
cout << "sum = " << sum << endl;
return 0;
}
初始化列表与赋值
- const成员的初始化只能在构造函数初始化列表中进行
- 引用成员的初始化也只能在构造函数初始化列表中进行
- 对象成员(对象成员所对应的类没有默认构造函数)的初始化,也只能在构造函数初始化列表中进行
类之间嵌套
第一种: 使用初始化列表。
class Animal {
public:
Animal() {
std::cout << "Animal() is called" << std::endl;
}
Animal(const Animal &) {
std::cout << "Animal (const Animal &) is called" << std::endl;
}
Animal &operator=(const Animal &) {
std::cout << "Animal & operator=(const Animal &) is called" << std::endl;
return *this;
}
~Animal() {
std::cout << "~Animal() is called" << std::endl;
}
};
class Dog {
public:
Dog(const Animal &animal) : __animal(animal) {
std::cout << "Dog(const Animal &animal) is called" << std::endl;
}
~Dog() {
std::cout << "~Dog() is called" << std::endl;
}
private:
Animal __animal;
};
int main() {
Animal animal;
std::cout << std::endl;
Dog d(animal);
std::cout << std::endl;
return 0;
}
第二种:构造函数赋值来初始化对象。
Dog(const Animal &animal) {
__animal = animal;
std::cout << "Dog(const Animal &animal) is called" << std::endl;
}
- 类中包含其他自定义的class或者struct,采用初始化列表,实际上就是创建对象同时并初始化
- 而采用类中赋值方式,等价于先定义对象,再进行赋值,一般会先调用默认构造,在调用=操作符重载函数。
无默认构造函数的继承关系中
class Animal {
public:
Animal(int age) {
std::cout << "Animal(int age) is called" << std::endl;
}
Animal(const Animal & animal) {
std::cout << "Animal (const Animal &) is called" << std::endl;
}
Animal &operator=(const Animal & amimal) {
std::cout << "Animal & operator=(const Animal &) is called" << std::endl;
return *this;
}
~Animal() {
std::cout << "~Animal() is called" << std::endl;
}
};
class Dog : Animal {
public:
Dog(int age) : Animal(age) {
std::cout << "Dog(int age) is called" << std::endl;
}
~Dog() {
std::cout << "~Dog() is called" << std::endl;
}
};
- 由于在Animal中没有默认构造函数,所以报错,遇到这种问题属于灾难性的,我们应该尽量避免,可以通过初始化列表给基类的构造初始化。
类中const数据成员、引用数据成员
- 特别是引用数据成员,必须用初始化列表初始化,而不能通过赋值初始化!
class Animal {
public:
Animal(int age, std::string name) : age_(age), name_(name) {
std::cout << "Animal(int age) is called" << std::endl;
}
private:
int &age_;
const std::string name_;
};
// enum class
enum class EntityType {
Ground = 0,
Human,
Aerial,
Total
};
void foo(EntityType entityType)
{
if (entityType == EntityType::Ground) {
/*code*/
}
}
- 可以指定对象具有构造函数和析构函数,这些构造函数和析构函数在适当的时候由
- 编译器自动调用,这为管理给定对象的内存提供了更为方便的方法。
- 资源在析构函数中被释放
- 该类的实例是堆栈分配的
- 资源是在构造函数中获取的。
RAII代表“资源获取是初始化”。常见的例子有:
文件操作
智能指针
互斥量
为了使用copy-swap,我们需要三件事:
1.一个有效的拷贝构造函数
2.一个有效的析构函数(两者都是任何包装程序的基础,因此无论如何都应完整)以及交换功能。