【C++11】学习笔记(二)

通用为本

《C++11》第三章。(杂事太多,看的好慢。。)

继承构造函数

  • using关键字的使用

    通过using关键字,使用基类函数。这个之前没有用过。。于是,写了代码亲测一下效果。

    #include<iostream>
    using namespace std;
    struct Base {
    
    	void fun(double i) { cout << "Base : " << i << endl; }
    };
    struct Deriver : Base
    {
    	using Base::fun;
    	void fun(int i) { cout << "int Deriver : " << i << endl; }
    };
    void main()
    {
    	Deriver d;
    	d.fun(4);   // Deriver : 4
    	d.fun(2.5); // Base : 2.5
    	system("pause");
    }
    

    通过这样的方式,可以实现基类函数和父类函数的重载。

顺手复习了一下继承相关的的知识点:

  • 父类指针指向子类时:

    • 父类指针只能使用自己的成员函数/变量
    • 如果,父类的虚函数被子类重载,则调用子类函数
    • 如果,非虚函数被重写,父类指针调用的依旧是父类的函数
  • 虚析构函数

    这个也是查资料的时候看到的,之前也没有遇到过,这里补充一下。

    当用父类指针指向子类对象时,需要使用。因为delete父类指针指向的内存时,调用的依旧是父类的析构函数,并不能直接调用子类的析构函数。从而导致了内存泄露。

    #include<iostream>
    #include<string>
    using namespace std;
    struct Base {
    	virtual ~Base() { cout << "Base : 析构函数" << endl; }  //这里使用了虚析构函数
    	virtual void fun(double i) { cout << "Base : " << i << endl; }
    };
    struct Deriver : Base
    {
    	~Deriver() { cout << "Deriver : 析构函数" << endl; }
    	void fun(int i) { cout << "int Deriver : " << i << endl; }
    	void fun(double i) {
    		cout << "double Deriver : " << i << endl;
    	}
    };
    void main()
    {
    	Deriver *d = new Deriver;
    	delete(d);
    	cout << "----------------------------------" << endl;
    	Base* b = new Deriver;
    	delete(b);              //分别调用子类和父类的析构函数。                    
    	cout << "----------------------------------" << endl;
    	system("pause");
    }
    

    上述代码,如果不使用虚析构函数,delete(b)只调用父类的析构函数。

  • 虚继承

    如果一个派生类从多个基类派生,而这些基类又有一个共同的基类,则在对该基类中声明的名字进行访问时,可能产生二义性

    (图片资源来自网络)

    C C;
    C.B;     //error
    c.B::b;  //error,存在二义性
    c.B1::b; //OK
    c.B2::b; //OK
    

    C类中有两个B类的备份。分别来自B1和B2的备份。

    如果要想让C类只有一个备份,就需要使用虚继承

    class B{public: int B;}
    class B1 : virtual public B{private	: int b1;}
    class B2 : virtual public B{private	: int b2;}
    class C : public B1, public B2{private	: int c;}
    

    使用虚继承后,C中只有一个B类的备份。不论使用c.B::b; c.B1::b; c.B2::b;这三个那种形式,最后都指向同一块内存。

using继承构造函数

  • using继承构造函数

    言归正传,C++11吸收了using关键字可以使用父类成员的想法,将using扩展到构造函数上。

    struct A
    {
    	A(int i){}
    	A(int i,double d){}
    	A(int i,const char*){}
    	//...
    };
    struct B : A
    {
    	using A::A;
    	int d{0}
    };
    int main()
    {
    	B b(256);  // b.d = 0
    }
    

    总结:

    1. 使用using之后,继承构造函数无法对派生类的成员函数进行初始化。

    2. 如果需要需要给派生类成员赋值,则重写派生类构造函数。或者结合C++11新特性,给成员变量初始化。

    3. 当派生类有两个基类,且含有相同参数的构造函数。如果对两个基类都使using继承狗在函数,则会产生二义性。

      struct A {A(int){}}; 
      struct B {B(int){}};
      struct C : A,B {
      	using A::A;
      	using B::B;
      	C(int){}    // 用来消除二义性
      }
      
      

委派构造函数

为了解决,多版本构造函数之间大量的重复代码,C++11增加了委派构造函数。

这里看到一个骚操作,类是不允许调用自己的构造函数的。但是,这个骚操作可以:

class A
{
public:
	A() { info(); }
	A(int i) { new(this) A(); }    //可以编译通过,没搞清原理
	A(double a) { A(); }           //可以编译通过
	//A(char a) { this->A(); }      //不可以编译
private:
	void info() { cout << "1111" << endl; }
};

经测试,虽然不能通过this在构造函数调用构造函数,但是可以直接调用。这大概是因为我使用的编译器已经默认支持委派构造函数导致的。之前版本的编译器,或许并不支持。

new(this) A();

实质是,直接拿了this指向的内存,从中调用函数。破坏了编译器的安全机制

委派构造函数的使用流程:

  1. 首先抽象一个目标构造函数(构造函数冗余的部分)

  2. 委派构造函数在函数的初始化列表上使用目标构造函数(测试,可以在函数体内直接调用)

注意:委派构造函数不能用初始化列表

struct Rule
{
	int i;
	Rule(int a):i(a){}
	Rule():Rule(40),i(1){}  //不能通过编译
}

给出一个实例:

class Info
{
public:
	Info() : Info(1) {}   //委派函数
	Info(int i) : Info(i,'a') {}   //委派函数 + 目标函数
	Info(char a) : Info(1,a) {}
private:
	Info(int i, char e) : type(i), name(e) {}   // 目标函数
	int type;
	char name;
};

注:当构造函数比较多的时候,是可以有多个委派函数。或者将委派函数作为目标函数,形成一个链状委派结构。

最后给一个委派构造函数在实际中的应用。

(构造模板函数)《C++11》P85.

class TDConstructed {
	template<class T> TDConstructed(T first, T last) :l(first, last) {}
	list<int> l;

public:
	TDConstructed(vector<short>& v) :TDConstructed(v.begin(), v.end()) {}
	TDConstructed(deque<int>& d) :TDConstructed(d.begin(), d.end()) {}
};

通过模板构造函数可以很好的实现构造函数的泛型编程。

另外如果构造函数使用try,那么目标函数抛出的异常也可以被捕捉到。

class ECExcept {
	ECExcept(int i, double d) {
		cout << "going to throw!" << endl;
		throw 0;
	}
	int type;
	double data;
public:
	ECExcept(double d) 
	try: ECExcept(1,d){
		cout << "Run the body." << endl;
	}
	catch(...) {
		cout << "caught exception." << endl;
	}
};

运行结果:

going to throw
caught exception

右值引用:移动语义和完整转发

  • 左值引用、右值引用(&&)

    左值: 能够取地址、有名字的

    右值:不能取地址,没有名字的值

    将亡值:右值引用

    纯右值:能理解,但是不知道怎么表达,唉,抄书抄书用于辨识零时变量和一些不跟对象关联的值。

    右值引用相当于给零时对象起名,具体可看博客右值引用

    #include<iostream>
    #include<string>
    
    using namespace std;
    
    struct Copyable {
    	Copyable() { cout << "Construct : " << endl; }
    	Copyable(const Copyable& o) {
    		cout << "Copied" << endl;
    	}
    };
    
    Copyable ReturnRvalue() {
    	//Copyable b = Copyable();
    	return Copyable();
    }
    void AcceptVal(Copyable a) { }
    void AcceptRef(const Copyable&a) { }
    void main()
    {
    	cout << "Pass by value: " << endl;
    	AcceptVal(ReturnRvalue());
    	cout << "Pass by reference: " << endl;
    	AcceptRef(ReturnRvalue());
    	cout << __cplusplus << endl;
    	system("pause");
    }
    

    运行结果:

      Pass by value:
      Construct :
      Pass by reference:
      Construct :
    

    通过结果可以看出,无论是将临时变量以左值引用作为参数,还是以左值作为参数,都只调用一次构造函数(在构造临时变量时)。并没有调用拷贝构造函数,构造新的对象作为参数。

    这应该是VS2019编译器对C++的优化。

  • 移动构造函数

    移动构造函数的目的,当函数返回一个对象的时候,不需要调用拷贝构造函数,为对象重新分配内存并将返回的临时对象的值拷贝过来,然后将临时对象析构。而移动构造函数是直接将临时对象绑定到新的对象上使用。

    但是,在实际测试的时候与《C++11》书上提到的结果有很大的出入。

    #include<iostream>
    #include<string>
    using namespace std;
    class HesPtrMem {
    public:
    	HesPtrMem():d(new int(0)) {
    		cout << "Consturct : " << ++n_cstr << endl;
    	}	
    	HesPtrMem(const HesPtrMem&h):d(new int(*h.d)) {
    		cout << "copy Consturct : " << ++n_cptr << endl;
    	}	
    	~HesPtrMem() {
    		cout << "Destruct : " << ++n_dstr << endl;
    	}
    
    	int* d;
    	static int n_cstr;
    	static int n_dstr;
    	static int n_cptr;
    };
    
    int HesPtrMem::n_cstr = 0;
    int HesPtrMem::n_dstr = 0;
    int HesPtrMem::n_cptr = 0;
    
    HesPtrMem GetTemp() { return HesPtrMem(); }
    void getH() { HesPtrMem a = GetTemp(); }
    void main()
    {
    	getH();
    	system("pause");
    }
    

    书上的结果:

      Construct: 1
      Copy construct: 1
      Destruct: 1
      Copy construct: 2
      Destruct: 2
      Destruct: 3
    

    上述代码,会调用几次构造函数(拷贝构造函数)和析构函数呢?
    理论上,构造函数调用1次,拷贝构造函数调用2次,析构函数调用三次。

    1. 首先,GetTemp() 函数显式调用构造函数,构造函数执行一次。

    2. GetTemp() 函数中,调用拷贝构造函数创建临时对象作为返回值。

    3. 调用拷贝构造函数,将临时对象的值拷贝给对象a。

    4. 析构过程就不详细说了。。。

    VS2019 测试结果:

      Construct: 1
      Destruct: 1
    

    实际上:在VS2019中,上述代码仅仅调用了一次构造函数和一次析构函数。

    1. 在GetTemp()显式调用时使用构造函数,创建的对象作为临时对象返回。

    2. 在getH()函数中,直接将临时对象与对象a绑定。不调用任何构造函数

    在书上的例子的基础上,稍微改动一下:

    #include<iostream>
    #include<string>
    using namespace std;
    class HesPtrMem {
    public:
    	HesPtrMem() :d(new int(0)) {
    		cout << "Consturct : " << ++n_cstr << endl;
    	}
    	HesPtrMem(const HesPtrMem& h) :d(new int(*h.d)) {
    		cout << "copy Consturct : " << ++n_cptr << endl;
    	}
    	HesPtrMem(HesPtrMem&& h) noexcept(true) :d(h.d) {
    		h.d = nullptr;
    		cout << "Move Consturct : " << ++n_mvtr << endl;
    	}
    	~HesPtrMem() {
    		if(d != nullptr){
    			delete d;
    		}
    		cout << "Destruct : " << ++n_dstr << endl;
    	}
    	int* d;
    	static int n_cstr;
    	static int n_dstr;
    	static int n_cptr;
    	static int n_mvtr;
    };
    
    int HesPtrMem::n_cstr = 0;
    int HesPtrMem::n_dstr = 0;
    int HesPtrMem::n_cptr = 0;
    int HesPtrMem::n_mvtr = 0;
    
    HesPtrMem GetTemp() {
    	HesPtrMem h;
    	cout << "" << __func__ << "   h.d: " << hex << h.d << endl;
    	cout << "" << __func__ << "  &h.d: " << hex << &h.d << endl;
    	return h;
    }
    void getH() {
    	HesPtrMem a = GetTemp();
    	cout << "" << __func__ << "   a.d: " << hex << a.d << endl;
    	cout << "" << __func__ << "  &a.d: " << hex << &a.d << endl;
    }
    void main()
    {
    	getH();
    	system("pause");
    }
    
    //  /Zc:__cplusplus
    
    

    《C++11》提到的运行结果:

      Construct: 1
      Resource from GetTemp: 0x603010
      Move construct: 1
      Destruct: 1
      Move construct: 2
      Destruct: 2
      Destruct: 3
    

    VS2019实际运行结果:

      Consturct : 1
      GetTemp   h.d: 0000018D78205C20
      GetTemp  &h.d: 00000036AAEFF6C8
      Move Consturct : 1
      Destruct : 1
      getH   a.d: 0000018D78205C20
      getH  &a.d: 00000036AAEFF808
      Destruct : 2
    

    相比之下,经过编译器优化,少调用了一次移动构造函数。

    我们在main()函数中直接调用GetTemp(),依旧调用了一次构造函数,一次移动构造函数。说明

    HesPtrMem a = GetTemp();
    

    这条语句没有调用任何构造函数,而是将临时变量直接和a绑定了。

    移动构造的实质,和浅拷贝有些类似,只拷贝指针对象的值,而指针指向的内存空间并没有进行拷贝。而是将内存地址赋值给新对象。为防止指针空间被被移动对象析构,要将其指针标量赋值为nullptr。

  • move:强制转化为右值

    std::move()的作用就是强制将左值转换成右值。

    测试,对基本数据类型使用move后原数据不会有改变,但是string等对象move后,数据就不存在了,所以不能继续使用该变量

    这里要注意,使用移动构造函数后,会调用移动构造函数,原左值的指针会被赋值为NULL,或被析构。因此不能再继续使用。

  • 移动构造的noexpect

    调用移动构造函数的过程中,一般是不允许出现异常的。如果移动构造函数未完成,导致异常,可能会导致一些指针成为野指针。因此,需要在移动构造函数添加noexcept关键字。当出现异常时,直接停止程序。

    我们也可以使用move_if_noexcept(),用来检测移动构造函数是否含有noexcept属性,如果没有,则调用拷贝构造函数。

    #include<iostream>
    #include<string>
    using namespace std;
    struct MayThrow
    {
    	MayThrow() {}
    	MayThrow(const MayThrow&) {
    		cout << "MayThrow Copy Construct: " << endl;
    	}
    	MayThrow(MayThrow&&){
    		cout << "MayThrow Move Construct: " << endl;
    	}
    };
    struct NotThrow
    {
    	NotThrow() {}
    	NotThrow(const NotThrow&) {
    		cout << "NotThrow Copy Construct: " << endl;
    	}
    	NotThrow(NotThrow&&) noexcept{
    		cout << "NotThrow Move Construct: " << endl;
    	}
    };
    void main()
    {
    	MayThrow m;
    	NotThrow n;
    
    	MayThrow mt = move_if_noexcept(m);
    	NotThrow nt = move_if_noexcept(n);
    
    	cout << __cplusplus << endl;
    	system("pause");
    }
    

    运行结果:

      MayThrow Copy Construct:
      NotThrow Move Construct:
    

    move_if_noexcept是一种牺牲性能保证安全的策略。

  • RVO/NRVO

    在移动构造的例子中,会发现实际运行代码的过程,和书上的结果有些许差异。这就是因为编译器进行了RVO(Return Value Opitimization)优化。而书中运行例子均关闭了该优化。

    当把一个返回值赋值给一个新定义的对象时,不调用任何构造函数,而是直接“临时对象”的内存。

    之前遇到很多测试结果和书上结果不一致的地方,猜测是因为VS优化导致的。书上在这个地方有解释。确实是!!

  • 完美转发

    将模板函数接受的参数,不做任何处理,直接提供给目标函数(在模板函数中调用该函数)使用。不用创建临时对象,不用调用构造函数,直接提供给目标函数使用。

    • C++11为了实现完美转发,引入了新得规则——引用折叠。

      typedef const int T;
      typedef T& TR;
      TR& v = 1;   //C++98版本 不能通过编译。
      

      折叠规则:

      TR的类型定义声明v的类型v的实际类型
      T&TRA&
      T&TR&A&
      T&TR&&A&
      T&&TRA&&
      T&&TR&A&
      T&&TR&&A&&

      实例:

      void IamForwording(x& && t){
      	IrunCodeActually(static_cast<X& &&>(t));
      }
      //折叠后
      void IamForwording(x& t){
      	IrunCodeActually(static_cast<X&>(t));
      }
      
    • 一个完美转发的实例:

      #include<iostream>
      #include<string>
      using namespace std;
      
      void RunCode(int&& m) { cout << "rvalue ref" << endl; }
      void RunCode(int& m) { cout << "lvalue ref" << endl; }
      void RunCode(const int&& m) { cout << "const rvalue ref" << endl; }
      void RunCode(const int& m) { cout << "const lvalue ref" << endl; }
      
      template <typename T>
      void PerfectForward(T&& t) { RunCode(forward<T>(t)); }
      
      void main()
      {
      
      	int a;
      	int b;
      	const int c = 1;
      	const int d = 1;
      
      	PerfectForward(a);
      	PerfectForward(move(b));
      	PerfectForward(c);
      	PerfectForward(move(d)); //VS2019 这里有个警告。
      }
      

      运行结果:

        lvalue ref
        rvalue ref
        const lvalue ref
        const rvalue ref
      

      这里用到了forward方法,其本质和static_cast/move差不多,也就另外起了个名字。

显示转换操作符

  • 使用explicit关键字,防止系统隐式转换。

    #include<iostream>
    using namespace std;
    struct Rational1 {
    	Rational1(int n = 0, int d = 1) :num(n), den(d) {
    		cout << __func__ << " (" << num << "/" << den << ") " << endl;
    	}
    
    	int num;    //Numerator(被除数)
    	int den;     // Denominator(除数)
    };
    struct Rational2 {
    	explicit Rational2(int n = 0, int d = 1) :num(n), den(d) {
    		cout << __func__ << " (" << num << "/" << den << ") " << endl;
    	}
    	int num;
    	int den;
    };
    void DisPlay1(Rational1 ra) {
    	cout << "Numerator: " << ra.num << " Denominator: " << ra.den << endl;
    }
    void DisPlay2(Rational2 ra) {
    	cout << "Numerator: " << ra.num << " Denominator: " << ra.den << endl;
    }
    void main()
    {
    	Rational1 r1_1 = 11;
    	Rational1 r1_2 = 12;
    
    	Rational2 r2_1 = 21;     // 无法通过编译
    	Rational2 r2_2(22);
    
    	DisPlay1(1);
    
    	DisPlay2(2);             //无法通过编译
    	DisPlay2(Rational2(2));
    
    	return;
    }
    

    因为Rational2需要显示调用构造函数,因此不能用过 = 等操作符隐式转换。

    这么做,主要是为了增加代码的可读性。review更加方便一点。

  • C++11将其扩展到自定义操作符上:

    class ConvertTo{};
    class Convertable{
    public:
    	explicit operator ConvertTo() const { return ConvertTo(); }
    };
    
    void Func(ConvertTo ct) {}
    
    void test() {
    	Convertable c;
    	ConvertTo ct(c);
    	ConvertTo ct2 = c;   // 不能通过编译
    	ConvertTo ct3 = static_cast<ConvertTo>(c);
    	Func(c);   //不能通过编译
    } 
    
    

    使用显示类型转换,没有完全禁止类型之间的转换。而是禁止了使用拷贝构造函数和非显式类型转换不被允许。即,不能通过赋值表达式或者函数参数的方式来产生一个目标类型。

列表初始化

  • 初始化列表

    //C++98 初始化
    int arr[5] = {0};
    int arr[] = {0,1,2,3,4,5};
    
    //C++11 初始化
    int b[] {1,2,3,};   //C++98 编译失败
    vector<int> c{1,2,3}; //C++98 编译失败
    map<int.float> d = {{1,1.1},{2,2.1},{3,3.1},{4,4.1}};  //C++98编译失败
    

    总结:c++11丰富了变量的初始化,大致可以用以下几种方式

    1. = 加上赋值表达式 : 例如, int a = 3 + 4;
    2. = 加上{} : 例如, int a = {3 + 4};
    3. ()表达式列表 : 例如, int a(5);
    4. {}初始化列表 : 例如, int a{4+5};
    5. {}也可用于new操作:int * i = new int{3};
  • 自定义类型使用初始化列表

    包含头文件<initializer_list>头文件,并在自定义类型的构造函数中,使用initializer_list作为参数,就可使自定义类型使用初始化列表(用{}进行初始化)

    #include<iostream>
    #include<vector>
    #include<initializer_list>
    using namespace std;
    
    enum Gender {boy,girl};
    
    class People {
    public:
    	People(initializer_list<pair<string, Gender>> l) {
    		auto i = l.begin();
    		for (; i != l.end(); i++) {
    			data.push_back(*i);
    		}
    	}
    private:
    	vector<pair<string, Gender>> data;
    };
    
    void main()
    {
    	People ship2012 = { {"Garfield",boy},{"HelloKitty",girl} };
    }
    

    代码中pair不是很了解,之后看STL时,分析分析源码!!!

  • 对于旧代码使用初始化列表

    可以通过一个全局函数,用初始化列表作为参数,构造已有的对象。(我把这个函数叫做伪构造函数,我自己起的名字

    #include<initializer_list>
    using namespace std;
    
    void Fun(initializer_list<int> iv){ }
    
    int main()
    {
    	Fun({1,2});
    	Fun({});
    }
    
  • 构造可以接受初始化列表的操作符

    #include<iostream>
    #include<vector>
    #include<initializer_list>
    using namespace std;
    
    class Mydata {
    public:
    	Mydata& operator [] (initializer_list<int> l) {
    		for (auto i = l.begin(); i != l.end(); i++) {
    			idx.push_back(*i);
    		}
    		return *this;
    	}
    
    	Mydata& operator = (int v) {
    		if (idx.empty() != true) {
    			for (auto i = idx.begin(); i != idx.end(); i++) {
    				d.resize((*i > d.size()) ? *i : d.size());   //设置vector的大小
    				d[(*i) - 1] = v;
    			}
    			idx.clear();
    		}
    		return *this;
    	}
    
    	void print() {
    		for (auto i = d.begin(); i != d.end(); i++) {
    			cout << *i << "   ";
    		}
    		cout << endl;
    	}
    
    private:
    	vector<int> idx;
    	vector<int> d;
    };
    
    void main()
    {
    	Mydata d;
    	d[{2, 3, 5}] = 7;
    	d[{1, 4, 5, 8}] = 4;
    
    	d.print();
    
    	system("pause");
    }
    

    这段代码对我而言很是新颖,当然,源码也不难理解,就不多做解释。以后多看看,记住这种骚操作。

  • 初始化列表作为返回值

    vector<int> Func(){return {1,3};}
    deque<int> Func2(){return {1,3};}
    const vector<int>& Func3(){return {1,3};}  //注意,这里必须加const
    

    注:返回引用时,必须加const

  • 防止类型收窄

    定义:类型收窄指的是一些可以使得数据变化或者精度丢失的隐式类型转换。

    可能导致类型收窄的典型情况:

    1. 从浮点数转换为整型。例如, int a = 1.2,结果 a = 1。
    2. 从高精度浮点数转换为低精度浮点数。例如, double 转换位 float
    3. 从整数(或者非强类型的枚举)转换为浮点型,如果整型数大到浮点数无法精确地表示,则也可以视为类型收窄
    4. 从整型(或者非强类型的枚举)转化为较低长度的整型,比如:unsigned char = 1024。1024不能被一般长度为8位的unsigned char 所容纳,所以也可以视为类型收窄。
  • C++11使用初始化列表,防止类型收窄

    const int x = 1024;
    const int y = 10;
    
    char a = x;                    //收窄,但是可以通过编译
    char* b = new char(1024);      //收窄,但是可以通过编译
    
    char c = {x};                  //收窄,无法通过编译
    char d = {y};                  //通过编译
    unsigned char e {-1};          //收窄,无法通过编译
    
    float f {7};                   //可以通过编译
    int g {2.0f};                  //收窄,无法通过编译
    float *h = new float{1e48};    //收窄,无法通过编译
    float i = 1.2l;                //可以通过编译
    

    总结:C++11中,初始化列表是唯一一种可以防止类型收窄的初始化方式

    增加代码的安全性!!

POD类型(plain old data)

  • 平凡数据类型:

    1. 拥有平凡的默认构造函数和析构函数

      这里平凡默认构造函数和平凡析构函数指的是系统生成的默认构造函数和析构函数。

      如果,在定义类的时候显式的声明了构造函数或析构函数,即使什么都不干,那么该方法就不再是“平凡”的。

    2. 拥有平凡的拷贝构造函数和移动构造函数(同 1)

    3. 拥有平凡的拷贝赋值运算和移动赋值运算符(原理同 1 )

    4. 不能包含虚函数以及虚基类

  • 通过is_trivial判断是否为平凡数据

    #include<type_traits>
    #include<iostream>
    using namespace std;
    
    struct Trivial{
    	int a[10];
    }
    struct Trivial2{
    	Trivial t;
    	int a[10];
    }
    
    void main(){
    	cout << is_trivial<Trivial>::value<<endl;
    }
    
  • 标准布局

    定义:

    1. 所有非静态成员有相同的访问权限(所有非静态成员必须属于public\private\protected其中之一)

    2. 在类或者结构体继承时,需要满足:

      • 派生类中有非静态成员,且只有一个仅包含静态成员的基类
      • 基类有非静态成员,而派生类没有非静态成员
    3. 类中第一个非静态成员的类型与其基类不同

      struct A : B{B b;};           //不是标准布局类型
      struct C : B{int a ; B b;};   //是标准布局类型
      

      该原则是因为:

      1. 在C++标准中,如果基类没有成员,则派生类的第一个成员与基类共享地址。
      2. C++标准还要求,类型相同的对象,必须地址不同。(即基类B地址与对象b地址必须不同)

      综合上述两条原则:

      1. 当派生类第一个成员和基类一致时,派生类会为基类分配相应的空间。
      2. 当派生类第一个成员不是基类时,派生类的第一个成员与基类共享地址,不会多分配内存。

      经测试,编译器经过优化,不论是不是按照这个标准写,结果都是共享内存。

    4. 没有虚函数和虚基类

    5. 所有非静态数据成员均扶额标准布局类型,其基类也符合标准布局。

    判断是否没标准布局:

      ```C++
      template<typename T> struct std::is_standard_layout;
    
      //使用
    
      cout << is_standard_layout<Slayout>::value<<endl;  // 1 : 是  0 : 否
      ```
    
  • POD数据

    当自定义数据类型同时满足平凡类型和标准型,就认为该数据类型为POD数据。

    判断是否POD:

    
    typedef <typename T> struct std::is_pod;
    
    cout << is_pod<U>::value <<endl;
    
    

    POD作用:

    1. 字节复制,我们可以很安全的使用memset和memcpy对POD进行初始化
    2. 提供对C内存布局兼容
    3. 保证静态初始化的安全有效

非受限联合体

  • 联合体(Union)

    在一个联合体内,可以定义多种不同的数据类型,这些数据类型共享相同的内存空间。

    C++98 中联合体的一些限制:

    1. 联合体内不能有非POD数据类型
    2. 不允许联合体内有静态或引用类型的成员

    C++11移除了上述限制——非受限联合体。但是,静态数据成员,在实际编程中还是不能用。

  • 初始化

    C++11删除了一些非限制联合体的默认函数。

    当联合体中含有非平凡成员(拥有非平凡构造函数),则非受限联合体的默认构造函数会被编译器删除。

    union T{
    	string s;
    	int n;
    };
    
    void main(){
    	T t;    //编译失败,因为T的构造函数被删除
    }
    

    解决办法:

    union T{
    	string s;
    	int n;
    public:
    	T(){new(&s) string;}
    	~T(){s.~string();}
    };
    
    int main()
    {
    	T t;
    }
    

    注意: 析构函数必须和构造的类型一致

  • 枚举式的类

    匿名非限制联合体运用于类的声明中。

用户自定义字面量

为了解决我我们给自定义数据作为参数函数传值时,需要先创建对象(或者临时对象),再进行传值。

使用自定义字面量,即不再需要手动创建对象,直接将字面量作为参数。

#include<iostream>
#include<cstdlib>

using namespace std;

typedef unsigned char uint8;

struct RGBA
{
	uint8 r;
	uint8 g;
	uint8 b;
	uint8 a;

	RGBA(uint8 R,uint8 G, uint8 B,uint8 A=0):
		r(G), g(G), b(B), a(A){}
};

RGBA operator ""_C(const char* col, size_t n) {
	const char* p = col;
	const char* end = col + n;
	const char* r, * g, * b, * a;

	r = g = b = a = nullptr;

	for (; p != end; p++)
	{
		if (*p == 'r') r = p;
		else if (*p == 'g') g = p;
		else if (*p == 'b') b = p;
		else if (*p == 'a') a = p;
	}

	if ((r == nullptr) || (g == nullptr) || (b == nullptr))
		throw;
	else if (a == nullptr)
		return RGBA(atoi(r + 1), atoi(g + 1), atoi(b + 1));
	else 
		return RGBA(atoi(r + 1), atoi(g + 1), atoi(b + 1),atoi(a+1));
}

std::ostream& operator<<(std::ostream& out, RGBA& col) {
	return out << "r: " << (int)col.r
		<< " , g: " << (int)col.g
		<< " , b: " << (int)col.b
		<< " , a: " << (int)col.a << endl;
}

void blend(RGBA&& col1, RGBA&& col2) {
	cout << "blend" << endl << col1 << col2 << endl;
}

void main()
{
	blend("r255 g255 b255"_C, "r12 g25 b1 a55"_C);
	system("pause");
}

  • 自定义字面量规则:

    1. 字面量为整型:

      字面量操作符函数只接受unsigned long long 或者 const char* 为参数。

      如果字面量超出unsigned long long 的范围,则转换为const char* 处理

    2. 字面量为浮点数:

      字面量操作符函数只接受long double 或者 const char* 为参数。(越界处理同上)

    3. 字面量为字符串:

      字面量操作符函数只接受(const char*, size_t)

    4. 字面量为字符:

      字面量操作符函数只接受char

    注:自定义字面量后缀最好以下划线或者固定字符开头,避免和系统预定义的后缀冲突

内联名字空间

用于解决使用包含子空间的名字空间时,声明过于麻烦。以及不能在不同的名字空间对模板进行特化的问题。

#include<iostream>
using namespace std;

namespace Jim {
	inline namespace Basic {    // 1)
		struct Knife { Knife() { cout << "Knife in Basic." << endl; } };
		class CorkScrew {};
	}

	inline namespace Toolkit {
		template<typename T> class SwissArmyKnife {};
	}

	namespace other {
		Knife b;      // 2)  
		struct Knife { Knife() { cout << "Knife in other." << endl; } };
		Knife c;        // 3)
		Basic::Knife k;  // 4)
	}
}
namespace Jim {
	template<> class SwissArmyKnife<Knife> {};  //5)
}

using namespace Jim;
void main() {

	SwissArmyKnife<Knife> sknife;  //6)
	system("pause");
}

说明:

  1. 内联名字空间关键字 inline namespace

  2. 如果Basic没有声明为内联,这里编译不能通过。声请内联后,相当于Basic::Knife。同样,当另一内联子空间声明了同一自定义数据类型时,再直接使用Knife,而标记字空间会因为二义性报错。

  3. 这里使用局部变量

  4. 强制使用 Basic::Knife

  5. 因为Basic是内联,因此,这里Knife相当于 Basic::Knife。

一个使用的例子:

#include<iostream>
using namespace std;

namespace Jim {
#if __cplusplus >= 201103L
	inline
#endif
		namespace cpp11 {

		struct Knife { Knife() { cout << "Knife in old c++." << endl; } };
	}

#if __cplusplus < 201103L
	inline
#endif
		namespace oldcpp {
		struct Knife { Knife() { cout << "Knife in old c++." << endl; } };
	}

}
using namespace Jim;
void main() {
	Knife a;   //根据C++编译器版本选择合适的函数和方法。
	system("pause");
}

模板的别名

  • C++98: typedef [类型] [别名]

  • C++11:增加 using [别名] = [数据类型]

  • is_same<>::value 用来判断数据类型是否一致。 头文件 #include<type_traits>

一般化的SFINEA规则

SFINEA–Substitution failure is not an error(匹配失败不是错误)

template <int T> struct A {};

char xxx(int);
char xxx(float);

template <class T> A<sizeof(xxx((T)0))> f(T) {}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值