C++学习笔记-现代C++功能

  1. 当函数的形参是按值传递时,实参的顶层const属性以及引用属性在传入时都会被忽略,但是实参的底层const不会被忽略。当形参是指针或者左值引用时(T&),实参的顶层const或底层const都不会被忽略,但是实参的引用属性仍然被忽略。当形参是万能引用时(T&&),实参的顶层const和底层const也不会被忽略,但是实参的引用属性被忽略,此外,尽管此时形参是右值引用,但是传入的实参是左值时,形参会变成左值引用,传入的实参是右值时,形参也会变成右值引用。此外,volatile如同const一样的规则。凡是修饰参数本身的类型的修饰符,都不会被传递,而修饰本参数所指的的参数(比如指针或者引用)的修饰符,可以被传递;

  2. 模板类型推导中,如果函数的形参声明的是按值传递,则当传入一个数组名称时(const char[10]),函数形参会被推导为指针类型(const char*)。但是,如果函数形参声明为按引用传递时,此时传递一个数组给模板函数,则会推导成数组的引用(const char (&)[10]),这样的话,就可以创造一个模板,用来返回数组含有元素的个数:

    template<typename T, std::size_t N>
    constexpr std::size_t arraySize(T (&)[N]) noexcept
    {
    	return N;
    }
    

    此外,函数指针也与数组类似,当函数模板的形参按值传递时,函数名会转成函数指针,当函数模板的形参按引用传递时,传入的函数名会推导为函数引用;

  3. 其实上述推导规则直接按照变量初始化的规则来理解会更轻松,直接理解为使用一个实参来初始化一个auto类型或者auto*类型或者auto&类型的变量就可以了。auto推导与模板类型推导的唯一区别是对于大括号初始化的推导,在模板函数的参数中直接传入大括号初始化列表,则会编译错误,而使用大括号初始化列表则可以对一个auto类型的变量进行初始化。比如:

    auto x = {1}; // x的类型是std::inltializer_list<int>类型,并不是整形
    auto x{1}; // 同上(不过我在VS中发现这么定义的话,x显示为int类型)
    

    此时,如果希望可以直接将大括号初始化列表当做实参传给模板函数的形参,则需要在声明模板函数的时候使用std::initializer<T>类型作为形参类型。因此,为了防止错误,只有在必要的时候,才使用大括号初始化列表去初始化变量

  4. auto&& y = x;中,如果x是一个左值,那么y就被推导成一个左值引用,如果x是一个右值,那么y就被推导成一个右值引用;

  5. 当函数返回值使用auto或者lambda的形参声明中使用auto时,此时使用的是模板类型推导规则,而不是auto类型推导规则,也就是说此时大括号初始化列表是无法通过编译的:

    auto f()
    {
    	return {1, 2, 3}; // 无法通过编译,无法推导出返回值类型
    }
    
  6. const T*&类型与const T* const&不一样,前一种表示是一种引用类型,并且绑定了一个const T*类型的变量上,后一种表示一种const引用类型,并且绑定了一个const T*类型;

  7. 使用boost库的type_id_with_cvr<T>().pretty_name()可以获取类型的精确信息:

    #include <boost/type_index.hpp>
    template<typename T>
    void f(const T& param)
    {
    	using std::cout;
    	using boost::typeindex::type_id_with_cvr;
    	cout << "T =           "
    		 << type_id_with_cvr<T>().pretty_name()
    		 << '\n';
    	cout << "param =       "
    		 << type_id_with_cvr<decltype(param)>().pretty_name()
    		 << '\n';
    	......
    }
    
    

    这样,调用函数时就会打印出新参的确切类型,而且是精确类型,比标准库的typeid()要正确且精确;

  8. auto存储lambda表达式比使用std::function<>存储,执行效率和内存使用率方面都会有提升;

  9. 获取std::vector<bool>的某个元素千万不要用auto,而是直接指定类型为bool;

  10. 数据成员类内初始化不能够使用圆括号的形式初始化,只能使用等号或者大括号的形式初始化,因为使用圆括号初始化时,编译器可能认为你是在声明一个函数类型,此时它认为你声明的函数类型中的形参并没有写明类型,而是写入了一个值,从而引发错误;

  11. 此外,不过复制的类型只可以采用大括号或者小括号初始化,不能用等号初始化:

    std::atomic<int> ai1{0}; // 正确
    std::atomic<int> ai2(0); // 正确
    std::atomic<int> ai3 = 0; // 错误,但是在VS2019中没问题哎
    
  12. 使用大括号初始化会禁用内置类型的数据收缩转换:

    double x = 0.0;
    int y{x}; // 错误,从“double”转换到“int”需要收缩转换
    

    但是用其他类型初始化方式就不会报错,顶多报警告;

  13. 如果构造函数中有一个构造函数的形参使用的是std::initializer_list<>类型,则一定要注意在初始化此类的一个对象时,用圆括号和用大括号可能得到不一样的结果,大括号优先匹配有std::initializer_list<>类型的构函,即使不是精确匹配;

  14. 当使用std::tuple<>取出其中的某个元素时(如std::get<1>(...),这里的数值1意义不明),模板下标可以使用非限定枚举类型(如std::get<Red>(...));

  15. 使用= delete可以应用于普通的函数,这样就可以阻止由于隐式类型转换而造成的函数函数重载:

    bool isLucky(int number);
    bool isLucky(char) = delete;
    bool isLucky(bool) = delete;
    bool isLucky(double) = delete;
    
    if (isLucky(4))... // 正确
    if (isLucky('a'))... // 错误,拒绝char类型
    if (isLucky(true))... // 错误,拒绝bool类型
    if (isLucky(4.3))... // 错误,拒绝double和float类型
    
    

    如果上面没有将那些函数声明为删除的,则下面那些所有的表达式都能编译通过,但是可能就违背了函数的初衷;

  16. 模板特例化只能在名字空间作用域内实现,而不能在类作用域内实现;

  17. 如果一个函数返回一个容器类型(如std::vector<>,注意不是std::vector<>&类型),则最好在函数的返回处主动返回右值类型(也就是return std::move(容器)),这样会调用容器的移动构造函数。不过,可以直接返回容器,貌似编译器会自动使用移动构函(不过,在某些情况下不会使用移构,所以还是尽量手动返回右值引用类型的值)。但是,如果函数返回的是容器的引用类型,则肯定不会调用移动构函,会调用复制构函,如:

    class A{...};
    A& makeA()
    {
    	A a;
    	......
    	return a; // 虽然这里不应该返回一个局部变量的引用,但是这里只是为了解释
    }
    
    A makeA1()
    {
    	A a;
    	......
    	return a; // 或return std::move(a);
    }
    
    A v1 = makeA(); // 调用复制构造函数
    A v2 = makeA1(); // 调用移动构造函数
    
    

    也就是说函数返回的类型是值类型,则返回的类型有移构的话,就会自动调用移构,如果没有移构,就会调用复构。但是返回类型是引用类型的话,调用的构函一定是复构,不会是移构,因为引用是一个左值;

  18. 如果一个类定义的移动构造函数没有加上noexcept声明,则在默认情况下不会调用移构,但是可以通过使用std::move()来强制调用移构:

    class A
    {
    	public:
    	A() = default;
    	A(const A&) {std::cout << "copy\n";}
    	A(A&&) {std::cout << "move\n";}
    	// A(A&&) noexcept {std::cout << "move\n";}
    };
    
    A f1()
    {
    	A a;
    	......
    	return a; // 由于A中的移构没有加上noexcpet,所以此处默认调用的是默构
    	// return std::move(a) // 显式使用std::move()的话,就会调用移构了
    	
    

    如果移构的定义处主动加上noexcept的话,大部分情况下也会隐式调用移构的;

  19. 此外,如果给一个对象自定义swap()函数,则注意一般情况下都尽量加上noexcept声明,因为如果使用标准库的std::move()来交换两个此对象容器(注意,不是对象元素,是容器,比如此类型的两个vector),则标准库的std::move()是否是noexcept的,取决于用户自定义的那个针对对象的swap()是否是noexcept的。如果是noexcept的,则移动容器的时候会自动使用移动构造函数,从而优化了速度,否则就会使用复制构造函数;

  20. 不过,如果定义的函数中调用的其他函数没有noexcept声明,即被调用的那些函数可能会抛出异常,则此定义的函数永远不会是noexcept的,它会发射"路过"的异常。尽管如此,你也可以显式地把这个自定义函数加上noexcept声明,编译器不会报错,但是如果运行过程中一旦抛出了异常,则程序直接终止运行;

  21. C++11标准中,默认地,内存释放函数(如delete和delete[])和析构函数(无论是用户自定义的析构函数还是编译器自动生成的析构函数)都隐式地具备noexcept性质。析构函数唯一不具有隐式noexcept的场合是其类中的数据成员(包括继承而来的数据成员以及数据成员类型中包含的数据成员)所对应的类中显式地将析构函数定义为可抛出异常的(即显式地加上noexcept(false)声明);

  22. 再次强调,移动构造函数,swap()函数,内存释放函数和析构函数,noexcept对它们最有价值;

  23. constexpr对象一定是常量,且一定要在编译器确定其值,不然就无法编译。而constexpr函数可以运行在编译期,也可以运行在运行期。C++14中constexpr函数可以用于类的设置函数,即设置类中的元素值,且函数返回值是void。此外,constexpr还可用于构造函数,这样就可以创建一个编译期的类对象了;

  24. constexpr函数里面不允许有I/O类语句;

  25. 对于单个要求同步的变量或内存区域,使用std::atomic就足够了,但是如果有两个或者更多个变量或内存区域需要作为一整个单位进行操作时,就要动用互斥量了;

  26. 声明一个复制构造函数不会阻止编译器生成一个默认的赋值运算符操作,反过来也是。不过,移动操作不一样,声明一个移动构造函数,会阻止编译器默认生成一个移动赋值运算符,反过来也同样;

  27. 一般情况下,如果自定义了类中的复制构造函数、赋值操作运算符和析构函数中的任何一个,则另外两个也可能需要自定义。不过,继承体系中基类的虚析构函数除外,因为基类必须将析构函数定义为虚的,此时如果没必要,则可以不自定义复制构造函数和赋值操作运算符;

  28. 移动操作自动生成的条件:该类未声明任何复制操作(指复制构造函数和赋值操作运算符),该类未声明任何移动操作,该类未声明任何析构函数;

  29. 当定义一个基类时,如果该类可以使用默认的构造方式来处理成员,并且希望有移动语义,则可以将它们定义为default

    class Base
    {
    public:
    	virtual ~Base() = default; // 基类必须有虚析构函数
    	
    	// 定义移动操作
    	Base(Base&&) = default;
    	Base& operator=(Base&&) = default;
    
    	// 定义复制操作
    	Base(const Base&) = default;
    	Base& operator=(const Base&) = default;
    };
    

    这样,即将析构函数定义为虚的了,同时此类也有移动操作和复制操作,且都是用的编译器默认生成的方式。如果不主动定义的话,移动操作就不会被生成;

  30. 注意,一般情况下,即使一个类不需要定义复制操作和移动操作,而是想用编译器默认生成它们,此时也尽量使用= default;来将它们全部声明一遍,这样可以防止以后修改类的时候可能出现性能问题;

  31. C++11中析构函数默认为noexcept,且仅当基类的析构函数为虚的,子类默认生成的析构函数才为虚的;

  32. 在已经存在复制构造函数、赋值运算符或者析构函数任意一个的前提下,编译器默认生成另外两个的行为虽然还在,但是已经是属于废弃的,因此,定义的类中最好全部使用= default;来将这些都显式地声明一遍;

  33. 注意,拷贝初始化(比如用等号的初始化或者函数返回值或者传入函数实参)无法调用声明为explicit的构造函数,但是直接初始化可以调用。一般容器的push_back()都是用的拷贝初始化,而emplace_back()用的是直接初始化;

  34. 当下列条件成立时,容器使用emplace_back()极有可能比使用push_back()运行的更快:

    • 待添加的值是以构造而非赋值的方式加入容器;
    • 传递的实参类型与容器的类型不同;
    • 容器不会由于存在重复值而拒绝待添加的值。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值