《More Effective C++》 学习

Slogan —— Keep Learning

感悟

  • 多看书,多积累,努力实践,沉淀,工作后的加速度非常重要。即使身处996,只要是在学习自己认为有意义的,就不会觉得虚无
  • 意识到看此书是个不会马上有收益的事情,但总在无时不刻规范着程序员的设计思路与代码规范,沉淀中ing

导读

  • 当编译器不支持某些功能的情况下,使用宏定义macros,有利于后期的项目移植
    • 例如有些c++编译器不支持bool类型,可以采用typedef int bool,在不影响使用的情况下,还便于代码后期移植
  • 继承延伸出 —— 指针和引用的 静态类型与动态类型
  • 申请类(从堆上申请空间),用完不再用但没delete,导致内存泄漏memory leak。简单来说,因为程序员代码习惯差,导致一部分堆内存被霸占浪费

基础议题(Basics)

条款1:reference和pointer的区别

  • 对于需要随时更换代表对象,必须用pointer
  • reference只能在初始化时赋值,并不能为Null。即不存在 null reference
  • 引用为何不需要判断有效性?按照原文的意思,默认不会有傻子程序员写出null reference的情况
  • 用pointer比reference安全,除非你写的时候加上const,const Class& TestClass,不允许改值,避免出现修改引用原对象的值
    • #include<iostream>
      using namespace std;
      
      int main()
      {
      	int a = 3;
      	const int& b = a;
      	b = 2;
      	cout << a << endl;
      
      	return 0;
      }
  • 经过一番搜索,发现如下事实:引用的底层就是通过指针实现的。我不禁发出了惊叹声,那引用有什么存在的意义呢?show code!

    • class A
      {
      	friend A operator +(const A* a, const A* b);
      };
      
      A operator + (const A* a, const A* b)
      {
      
      }
      
      原因:
      1.当我发现这种写法不行(两个操作数都是指针类型),是因为库函数里会存在两个指针+的运算符
      2.比如a+b,不需要写成 &a + &b , 表现形式上更直观

条款2:最好使用c++转型操作符

  • C的转换方法为(int),会出现三种情况
    • ①发生不合理的转换,因为编译器不容易诊断错误类型 
    • ②(int)这样强转的写法,很容易在其他地方也用到,导致出现语义歧义
    • ③对人和工具来说,都更容易理解
  • static_cast和C旧式几乎一样
  • const_cast,只能改常量性,不支持继承关系的转化
  • dynamic_cast,只支持继承关系的转化,而且只支持子类转父类
  • reinterpret_cast和编译平台强关联,因此不具有移植性,基本上不会使用该操作,除非没办法

条款3:绝对不要以多态的方式处理数组

  • 不能用的原因一:当Derived数组作为参数传进函数时,用Base数组接收。编译器会认为两个元素相隔的距离是 i * sizeof(Base),而不是 i * sizeof(Derived),因此 baseptr + i 会导致数据出错
  • 不能用的原因二:当Derived数组作为参数传入函数时,用Base数组接收。当发生delete Base[]时,会逆序调用析构函数,此时涉及指针算数表达式的计算,同原因一,导致析构结果 “未定义” ,这是未知行为
  • 总结:多态数组和指针算数表达不能同时使用,会出现未知错误;建议一个具体类,不要继承另一个具体类,可避免这些问题。之前也碰到过想把派生类放在一起用,现在恍然大悟不能这么做

条款4:非必要不提供default constructor

  • 因为主动声明有参数的构造函数,因此无法用默认的构造函数,有以下两种解法:
    • 使用non-heap array
      #include<iostream>  
      #include<vector>  
      using namespace std;
      
      class Base
      {
      public:
      	int v;
      	//要指定参数,不然还是会用默认构造函数
      	Base(int vv)
      	{
      		v = -1;
      	}
      
      	/*如果用new Base,会用这个覆盖原生的构造函数
      	Base()
      	{
      
      	}*/
      };
      
      
      int main()
      {
      	Base test[] = {
      		Base(2)
      	};
      
      	return 0;
      }
    • 使用指针数组,是通过new,也叫heap-array(所有使用new的数据,都需要手动释放,否则会导致内存泄露)
      #include<iostream>  
      #include<vector>  
      using namespace std;
      
      class Base
      {
      public:
      	int v;
      	//要指定参数,不然还是会用默认构造函数
      	Base(int vv)
      	{
      		v = -1;
      	}
      
      	/*如果用new Base,会用这个覆盖原生的构造函数
      	Base()
      	{
      
      	}*/
      };
      
      
      int main()
      {
      	Base** test = new Base* [10];
      	for (int i = 0; i < 10; ++i)
      	{
      		test[i] = new Base(i);
      	}
      
      	return 0;
      }
    • 最后,要不要默认构造函数呢?
      • 不使用默认构造函数,可能会导致一些需要实例对象的STL没法使用
      • 在virtual base class的前提下,必须要有,不然派生类的构造函数要又臭又长
      • 但有默认构造函数,会导致出现一些无意义的ID(在必须有ID的前提下),所有member function 或者 外部的function,都需要对id进行合法性判断,使得可执行程序和程序库都变大了,会导致浪费

操作符(Operators)

条款5:对定制的“类型转换函数”保持警觉

  • c++内置的数据结构,我们无能为力
  • 常见的自定义类隐式转换(单自变量构造函数,隐式类型转换操作)
    • 存在单自变量的构造函数(允许把int转为Base)
      #include<iostream>  
      #include<vector>  
      using namespace std;
      
      class Base
      {
      public:
      	int v;
      	//要指定参数,不然还是会用默认构造函数
      	Base(int vv)
      	{
      		v = vv;
      	}
      
      	/*如果用new Base,会用这个覆盖原生的构造函数
      	Base()
      	{
      
      	}*/
      };
      
      
      int main()
      {
      
      	Base bs = Base(2);
      
      	//会偷偷生成一个Base(1),然后赋给bs
      	bs = 1;
      
      	return 0;
      }
    • 隐式类型转换操作:拥有奇怪名称的member function,例如 operator int , operator double
      #include<iostream>  
      #include<vector>  
      using namespace std;
      
      class Base
      {
      public:
      	int v;
      	//要指定参数,不然还是会用默认构造函数
      	Base(int vv)
      	{
      		v = vv;
      	}
      
      	/*如果用new Base,会用这个覆盖原生的构造函数
      	Base()
      	{
      
      	}*/
      
      	operator int()
      	{
      		return 2;
      	}
      };
      
      
      int main()
      {
      
      	Base bs = Base(2);
      
      	/* x == 4 */
      	int x = 2 * bs;
      	return 0;
      }
  • 为什么不要提供任何隐式转换函数?(上面两种隐式转换方法)
    • 根本问题在于,此类函数可能会编译器被偷偷地调用
      • 隐式类型转换操作:当发现Base并没有重载<<操作符的前提下,编译器会想各种办法(包括找出一系列可用<<的隐式转换结果)
        #include<iostream>  
        #include<vector>  
        using namespace std;
        
        class Base
        {
        public:
        	int v;
        	//要指定参数,不然还是会用默认构造函数
        	Base(int vv)
        	{
        		v = vv;
        	}
        
        	/*如果用new Base,会用这个覆盖原生的构造函数 
        	Base()
        	{
        
        	}*/
        
        	operator int()
        	{
        		return 3;
        	}
        };
        
        
        int main()
        {
        
        	Base bs = Base(2);
        
        	/* code will cout 3 */
        	cout << bs << endl;
        	return 0;
        }
        • 解决方案:不要写这种奇怪的member function
    • 解决方案
      • 用explicit来指定该构造函数只能被显示调用
      • 将单变量构造函数的数据类型,改成一个NewClassType
  • 总结:有两种隐式转换的方法(定义隐式转换函数,单变量构造函数)。对于定义隐式转换函数,不要这么写就可以;对于单变量构造函数,记住加explicit来定义单变量构造函数(以后的单变量构造函数,最好一定写成explicit)。也可以采用NewClassType,但这种方法不好,局限性太大

条款6:区别increment/decrement操作符的前置(prefix)和后置(postfix)形式

  • 后缀自增:const(为了禁止i++++这种写法),临时变量(增加拷贝时间),返回的是自增前的值(看具体的使用情况,对返回值的要求),有个无名形参int(用于重载)
  • 前缀自增:引用,返回自增后的结果
  • 性能,现代编译器已经优化的没区别了,让我们康康结果吧:很显然,没什么区别
    #include<iostream>  
    #include<vector>  
    #include <ctime>
    using namespace std;
    
    
    int main()
    {
    	int T = 20;
    	while (T--)
    	{
    		// 我发现先跑同一段代码和后跑同一段代码,时间不一样
    		// 于是我先让它们俩都不跑
    		for (int i = 1; i <= 1e8; i++);
    		for (int i = 1; i <= 1e8; ++i);
    
    
    		clock_t begin1 = clock();
    		for (int i = 1; i <= 1e9; i++);
    		clock_t end1 = clock();
    		double elapsed_secs1 = double(end1 - begin1) / CLOCKS_PER_SEC;
    		//cout << elapsed_secs1 << endl;
    
    		clock_t begin = clock();
    		for (int i = 1; i <= 1e9; ++i);
    		clock_t end = clock();
    		double elapsed_secs = double(end - begin) / CLOCKS_PER_SEC;
    		//cout << elapsed_secs << endl;
    
    		cout << elapsed_secs << " " << elapsed_secs1;
    		if (elapsed_secs < elapsed_secs1)	cout << "前缀优" << endl;
    		else
    		{
    			cout << "后缀优" << endl;
    		}
    	}
    
    
    	return 0;
    }
    
    1.065 1.061后缀优
    1.083 1.085前缀优
    1.06 1.061前缀优
    1.058 1.058后缀优
    1.066 1.08前缀优
    1.059 1.056后缀优
    1.06 1.058后缀优
    1.063 1.056后缀优
    1.065 1.065后缀优
    1.067 1.061后缀优
    1.06 1.067前缀优
    1.056 1.061前缀优
    1.057 1.058前缀优
    1.057 1.06前缀优
    1.065 1.062后缀优
    1.062 1.059后缀优
    1.061 1.057后缀优
    1.058 1.059前缀优
    1.079 1.062后缀优
    1.085 1.092前缀优

条款7:千万不要重载&&  ||  ,   这三个操作符【不要重载,说明能重载,但重载没法写的好,反而导致程序更难理解】

  • &&和||语法上允许重载,但会被 函数语法 代替 骤死式语法
    • 例如重载了&&,则if(exp1 && exp2)被编译器就解释为 if(exp1.operator&&(exp2))【如果重载的是member function】
  • 逗号表达式的结果以最右侧的值为代表
  • 编译器禁止以下操作符的重载
    • 【.】【.*】【::】【?:】 new delete sizeof 四个cast

条款8:了解各种不同意义的new和delete

  • new operator:①分配内存operator new ②调用构造函数
  • placement new可以代替operator new这个过程,决定了要在哪个地址开始,进行对象构建。运用于共享内存
  • delete operator:①调用析构函数 ②oprator delete
    • 对于RawData就不要调用析构函数,音粗只要operator delete就够了
    • 对于placement new返回的地址,用operator delete不合适(因此delete operator也不合适)。正确操作:直接调用构造函数,并自定义FreeFunction
  • 总结:new operator和delete operator是内建操作符,不能允许修改。但我们可以定制化operator new和 operator delete。 但new和delete就别想了,Always no way!!!

异常模块了解的少(项目里用的也少),暂时跳过


效率

条款16:谨记80-20法则

  • 学会分析是哪20%的代码耗时了
  • Tips:VS的诊断工具→事件,可以看每一行的执行时间,太牛逼辣!

条款17:考虑使用lazy evaluation(缓式评估)

  • lazy evaluation四个应用
    • 引用计数:对于s1="Hello World",s2=s1的情况下,只要s2没发生修改,我们就不用做急式评估(new and copy)
      #include<string>  
      #include<vector>  
      #include <ctime>
      using namespace std;
      
      void aa()
      {
      	for (int i = 1; i <= 1e9; ++i);
      }
      
      int main()
      {
      	string s1 = "Hello World";
      	string s2 = s1;
      	reverse(s2.begin(), s2.end());
      	return 0;
      }
      
      可以看到执行reverse后, s2.ptr发生了改变
      +s2._Mypair._Myval2{ _Bx = {_Buf = 0x000000d8481bf7a0 "Hello World" _Ptr = 0x6f57206f6c6c6548 < 读取字符串字符时出错。 > _Alias = 0x000000d8481bf7a0 "Hello World" } ... }	std::_String_val<std::_Simple_types<char>>
      +s2._Mypair._Myval2{ _Bx = {_Buf = 0x000000d8481bf7a0 "Hello World" _Ptr = 0x6f57206f6c6c6548 < 读取字符串字符时出错。 > _Alias = 0x000000d8481bf7a0 "Hello World" } ... }	std::_String_val<std::_Simple_types<char>>
      +s2._Mypair._Myval2{ _Bx = {_Buf = 0x000000d8481bf7a0 "dlroW olleH" _Ptr = 0x6c6f20576f726c64 < 读取字符串字符时出错。 > _Alias = 0x000000d8481bf7a0 "dlroW olleH" } ... }	std::_String_val<std::_Simple_types<char>>
      
    • 区分读和写:operator无法区分读还是写,用lazy-evaluation直到可确定答案为止。 TODO:不是很懂,应该设计条款30的代理类结合理解

    • Lazy-Fetching(缓式取出):变量初始化为Null,需要数据的时候再取需要的数据,不需要拷贝整个class;如果变量是const,可用mutable声明变量,也可以const_cast去掉常量性

    • Lazy-Expression-EValuation:例如矩阵乘法,如果我们不需要所有的数据(只需要一部分),那么我们可以定义一个数据结构(包含2个指针和1个枚举【记录操作方式,加减乘除】),非常优秀的优化思想

  • 总结:Lazy-Evaluation的核心思想,在于只有使用的时候才激活,非常适合只需计算一部分数据的场景

条款18:分期摊还预期的计算成本

  • cache优化访问(Map)
  • 提前申请足够多的空间,避免反复进行operator new操作
  • 读数据提前读一片(locality of reference)
  • 总结:本质上还是“空间换时间”,当频繁操作的时候,提前做好操作可以均摊后面的开销;运算结果不是总需要,lazy可以优化性能。要看具体的需求适合哪种方案 

条款19:了解临时对象的来源

  • 临时变量的两个来源
    • 隐式转换使得函数能被成功调用(编译器只接受const-to-reference的隐式转换)
      #include<iostream>
      #include<vector>
      using namespace std;
      
      //can't use this param.禁止这种写法,因为从char*转到string会产生临时变量,&的变化,并不能修改实参
      void reverse(string& s)
      {
      
      }
      
      //can use this param
      void reverse(const string& s)
      {
      
      }
      
      int main()
      {
      	char *s;
      	scanf("%s",s);
      	reverse(s);
      
      
      	return 0;
      }
    • 函数返回值会调用拷贝构造函数(我测了代码,发现C++编译器已经对返回值进行优化,没有临时变量的产生)
      #include<iostream>
      #include<vector>
      using namespace std;
      
      struct test
      {
      	test()
      	{
      		cout << "构造函数啦!" << endl;
      	}
      	~test()
      	{
      		cout << "析构啦!" << endl;
      	}
      	test(const test&)
      	{
      		cout << "拷贝构造啦!" << endl;
      	}
      };
      
      test testfunc()
      {
      	return test();
      }
      
      int main()
      {
      	test t = testfunc();
      
      	return 0;
      }
  • 总结:reference-to-const(隐式转换) , 函数返回值(大多数都优化)

条款20:协助完成“返回值优化(RVO)”

  • 函数返回值不可避免的要生成临时变量,再通过拷贝构造函数赋值给调用的地方。但C++编译器做了优化,叫做返回值优化,取消了临时变量生成,再加上内联,会更快
  • 何时RVO失效?
    • if-else返回值会失去优化效果
    • 提前定义后赋值

      #include<iostream>
      #include<vector>
      using namespace std;
      
      struct test
      {
      	test()
      	{
      		cout << "构造函数啦!" << endl;
      	}
      	~test()
      	{
      		cout << "析构啦!" << endl;
      	}
      	test(const test&)
      	{
      		cout << "拷贝构造啦!" << endl;
      	}
      };
      
      test testfunc()
      {
      	if (true)
      	{
      		test res = test();
      		return res;
      	}
      }
      
      int main()
      {
      	test t;
      	t = testfunc();
      
      	return 0;
      }
  • 结论:RVO可以减少拷贝(复制)构造函数的次数,属于编译器优化,但有限制条件

条款21:利用重载技术(overload)避免隐式类型转换

  • 调用函数的时候如果参数不完全一致,会发生隐式转换,此时就产生了临时变量

条款22:以符合形式操作符取代独身形式

  • 匿名变量比有名变量好优化
  • 复合版本理论上比独身版本少一个临时变量,但现在RVO已经可以这个临时变量的copy constructor

条款23:考虑使用其他程序库

  • stdio效率≥iostream
  • 如果发现是内存方面的问题,考虑其他对operator new和operator delete高效的程序库

条款24:了解virtual functions、multiple inheritance、virtual base classes、runtime type identification的成本

  • 虚函数不能声明为inline,一个是静态编译,一个动态,是矛盾的
  • 虚函数的前提下,每个实例类都会有一个指针指向virtual table,但共用一份virtual table
  • 多重继承下,会有虚函数的需求
    • 不用虚基类,导致多次调用构造函数,即多个Base
      #include<string>  
      #include<vector>  
      #include <iostream>
      using namespace std;
      
      class Base
      {
      public:
      	Base()
      	{ 
      		cout << " 调用构造函数嘞1! " << endl;
      	}
      	Base(int v)
      	{
      		cout << " 调用构造函数嘞2! " << endl;
      		BaseValue = v;
      	}
      	int BaseValue;
      };
      
      class Drived1 : public Base
      {
      
      };
      
      class Drived2 : public Base
      {
      
      };
      
      class Drived12 : public Drived1, public Drived2
      {
      public:
      	Drived12(int v)/* : Base(v)*/
      	{
      
      	}
      };
      
      int main(void)
      {
      	Drived12 TestClass = Drived12(600);
      	return 0;
      }
      
      output:
      调用构造函数嘞1!
      调用构造函数嘞1!
    • 用虚基类,则可以去除多次复制的问题(但每个类会多一个指针指向base)
      #include<string>  
      #include<vector>  
      #include <iostream>
      using namespace std;
      
      class Base
      {
      public:
      	Base()
      	{ 
      		cout << " 调用构造函数嘞1! " << endl;
      	}
      	Base(int v)
      	{
      		cout << " 调用构造函数嘞2! " << endl;
      		BaseValue = v;
      	}
      	int BaseValue;
      };
      
      class Drived1 :virtual public Base
      {
      
      };
      
      class Drived2 :virtual public Base
      {
      
      };
      
      class Drived12 : public Drived1, public Drived2
      {
      public:
      	Drived12(int v) : Base(v)
      	{
      
      	}
      };
      
      int main(void)
      {
      	Drived12 TestClass = Drived12(600);
      	return 0;
      }
      
      output:
      调用构造函数嘞2!
  • 运行时期类型辨识(RTTI):将该object存在virtual table的index==0处,保存该类的一些信息
  • 性质对象大小增加(sizeof)class数据量增加能否inline
    虚函数
    多重继承
    虚基类
    RTTI


技术(Techniques,Idioms,Patterns)

条款25:将constructor和non-member function虚化(并不是直接加virtual,只是实现和virtual相关的效果)

  • virtual constructor(似乎不能说上是很频繁使用的,频率较低。更像一种设计模式)
    • 在构造函数内,产生基于同一个base但是不同派生类的对象
    • virtual copy,调用拷贝构造函数
    • 如果基类的某一个虚函数返回的是指针或者引用,那么派生类允许返回类型为派生类指针
      #include<string>  
      #include<vector>  
      #include <iostream>
      using namespace std;
      
      class Base
      {
      public:
      	Base()
      	{ 
      	}
      	Base(int v)
      	{
      		BaseValue = v;
      	}
      	int BaseValue;
      	virtual Base* PrintfFunc()
      	{
      		cout << "Base* PrintfFunc" << endl;
      		return new Base();
      	}
      };
      
      class Drived1 :public Base
      {
      	virtual Drived1* PrintfFunc()
      	{
      		cout << "Drived1* PrintfFunc" << endl;
      		return new Drived1();
      	}
      };
      
      
      int main(void)
      {
      	Base* Testp = new Drived1();
      	Testp->PrintfFunc();
      	return 0;
      }
      
      output:
      Drived1* PrintfFunc
    • 对于不同的派生类,重载<<
      • 写一个ostream& operator<<(ostream& s,const NLComponent& c),然后调用c.printf(s),符合cout << value 的左侧端(当然是为了强制用cout才要这样写,其实也可以直接ptr->printf)

条款26:限制某个class所能产生的对象数量

  • 限制的方法
    • 方法一:利用static(限制数量为1)
      • 将构造函数设为private,通过一个friend函数/Static函数,获取唯一static实例(有点单例模式的味道)
        • static函数,不能用inline(从inline的实现机制去思考,会出现多份function local static变量)
      • 函数里的static变量,只有调用的时候才会诞生(实例化)
    • 方法二:构造函数加计数器(可自定义限制数量)
      • 当派生类包含基类的时候,就麻烦了
    • 方法三:构造函数私有化,提供一个public static方法负责new calss(可自定义限制数量)
      • 禁止派生继承
      • 要注意new的同时,要注意delete,可以考虑使用智能指针
  • 一个用来计数对象个数的Base Class : 通用化计数器
    • 如果A类也要计数器,B类也要,重复写相同的代码不是好事,因此引入模板编程,编写一个通用的计数器类,类似vector<int>,vector<string>之类的效果
      • 利用了私有继承的以下几个优点
        • base在derived中所有成员都是private的,派生类并不关心base
        • base* 不能指向 derived 的内存地址
      • 对于一些必要的数据,例如目前计数器的数量,可以通过using declaration来将方法从private→public

条款27:要求(或禁止)对象产生于Heap中

  • 要求对象产生在Heap中
    • 方法一:禁止析构函数
      #include <vector>
      #include <iostream>
      
      using namespace std;
      
      class Base
      {
      public:
      	Base() {
      		cout << "000000000000000" << endl;
      	}
      private:
      	~Base() {
      		cout << "111111111111111" << endl;
      	}
      };
      
      void func() 
      {
      	/*此处编译错误*/
      	Base test;
      }
      
      int main(void)
      {
      	//while (true)
      	//{
      		func();
      		cout << "22222222222222" << endl;
      	//}
      	return 0;
      }
    • 方法二:禁止所有构造函数,但写起来比较麻烦,因为构造函数有好多个
    • 导致的问题:
      • 无法继承,将private改成protected
      • 无法内含:不用Base而用Base*
  • 判断某个对象是否位于Heap内
    • 在operator new里修改bIsOnHeap是不可行的
    • 利用heap和stack的高低同样不行,不同的os不同,而且还有static区域的影响
    • 解决方案:使用map/hash_map保存所有通过new产生的空间的首地址(依赖于dynamic_cast)
  • 禁止对象产生于heap之中
    • 私有化operator new
  • 总结:禁止,要求,判断,三者都不存在具有可移植性的实现方案

条款28: smart porinters(智能指针)

  • Smart Pointer责任在本身即将被销毁的时候,释放指向的对象(如果计数器为0)
  • 用pass-by-value的方式传递auto_ptrs是个很糟糕的行为,当调用的函数结束的时候,vector就被清空了。因为在调用函数的时候,auto_ptr会移交所有权给形参,形参在函数结束的时候释放了所有空间。建议使用by reference-to-const,而不是pass-by-value
  • 两种解引操作符*和->
  • 测试Smart Pointer是否为null,对于dumb point可以直接用==nullptr或者==0,但是Smart Pointer不行
    • 方法一:定义isNull方法,但不是很好用
    • 方法二:隐式操作符void*,这种方案会导致两种不同的类可能会==
    • 方法三:重载!操作符,这个可行,但这个只是判断有没有,不代表两个指针真的是相同
  • 将Smart Pointer为Dumb Pointer
    • 方案一:operator T*(),这样会直接让client使用dumb指针,会出大麻烦
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值