天天看点

第十二章:类(二)

构造函数

构造函数是一类特殊的成员函数,它是在构造对象时调用的成员函数。构造函数的名称与类名相同,无返回类型,可以包含多个版本(重载)

class Str{

public:
    Str(){
        std::cout << "Constructor is called " << std::endl;
    }

    Str(int input){
        x = input;
    }

private:
    int x;
};
           

在C++11之后,标准引入了一种新的构造函数-代理构造函数

class Str{

public:
	// 这里需要注意下代理构造函数的执行顺序
    Str() : Str(3)					// 先执行 Str(3)
    {								// 再执行函数体
        std::cout << x << std::endl;
    }

    Str(int input){
        x = input;
        std::cout << x << std::endl;
    }

private:
    int x;
};
           

在构造函数中,我们通常需要对成员变量进行初始化和赋值,在不使用初始化列表的情况下,我们只能使用以下的方式

class Str{

public:
    Str(const std::string & val)
    {
        x = val;				// 并不高效,因为这里其实是赋值语句,而初始化语句被系统默认调用,并进行了默认的赋值
    }


private:
    std::string x;
};
           
这种方式的主要缺点是进行了一次无意义的默认值赋值操作

在使用初始化列表之后,我们可以将初始化与赋值结合在一起,从而减少不必要的操作,提升程序的性能,比如

class Str{

public:
    Str(const std::string & val): x(val)		// 高效的方式
    {
    }


private:
    std::string x;
};
           

对于初始化列表,我们需要注意以下几点

  • 通常情况下使用初始化列表可以提升系统性能
  • 一些情况下必须使用初始化列表,比如类中包含引用成员
  • 元素的初始化顺序与其声明顺序相关,与初始化列表中的顺序无关
  • 使用初始化列表可以覆盖类内成员初始化的行为,初始化列表的优先级要高于类内成员初始化

在构造函数中还存在一些特殊的构造函数,首先我们关注缺省构造函数。 缺省构造函数是不需要提供实际参数就可以调用的构造函数,比如

class Str{

public:
    Str(){

    }
    Str(int input = 3){

    }

private:
    int x;
};
           
注意以上两种都是缺省构造函数,但是在不提供参数进行构造时会报错,因为系统无法判断需要调用哪一个

关于缺省构造函数,我们需要注意以下几点

  • 如果类中没有提供任何构造函数,那么在条件允许的情况下,编译器会生成一个缺省构造函数
    注意,如果提供了带参数的构造函数,那么编译器不会生成无参数的缺省构造函数,这时如果使用缺省的方式构造,会报错
    注意,生成缺省构造函数需要在条件允许的情况下。典型的比如类内包含引用成员,在这种情况下是不允许生成缺省构造函数的,因为没有办法为引用成员初始化
  • 调用缺省构造函数时要避免

    most vexing parse

    class Str{
    
    public:
        Str(){
    
        }
    
    private:
        int x;
    };
    
    int main() {
        Str m();			// 注意,这玩意是函数声明,不是使用缺省构造函数来构造对象
    }
               
  • 可以使用

    default

    关键字来定义缺省构造函数
    class Str{
    
    public:
        Str() = default;			// 强制编译器在存在带参数构造函数的情况下生成缺省构造函数
       
        Str(int input): x(input)
        {
            
        }
    
    private:
        int x;
    };
               

之后我们关注单一参数构造函数,他在构造时可以有一些特殊的用法,比如

class Str{

public:
    Str(int input): x(input)
    {

    }

private:
    int x;
};

int main() {
    Str m = 3;
}
           

关于单一构造函数,我们需要注意以下几点

  • 单一参数构造函数可以视为一种类型转换函数
  • 可以使用

    explicit

    关键字避免求值过程中的隐式转换
    class Str{
    
    public:
        explicit  Str(int input): x(input)
        {
    
        }
    
    private:
        int x;
    };
    
    int main() {
        Str m = 3;					// 此时这里是非法的
    }
               

接下来我们关注拷贝构造函数。拷贝构造函数是一种接收一个当前类对象的构造函数

class Str{

public:
    Str() = default;
    Str(const Str& x)
        :val(x.val)
    {
    }

private:
    int val = 3;
};

int main() {
    Str m ;
    Str m2 = m;			// 调用拷贝构造函数
}
           

关于拷贝构造函数,我们需要注意以下几点

  • 拷贝构造函数会在涉及到拷贝初始化的场景被调用,比如:参数传递。因此要注意拷贝构造函数的形参类型。
    如果不加引用会导致无限调用拷贝构造函数,直接报错。其次,一般我们不希望拷贝构造函数对传入的对象进行修改,所以一般会加上

    const

    进行修饰
  • 如果没有显式提供拷贝构造函数,那么编译器会自动生成一个拷贝构造函数,自动生成的版本会依次对每个数据成员调用拷贝构造。
  • 可以使用

    default

    关键字来生成缺省的拷贝构造函数
    class Str{
    
    public:
        Str() = default;
        Str(const Str&) = default;
    
    private:
        int val = 3;
    };
               

最后,我们关注C++11引入的一种新的构造函数-移动构造函数。移动构造函数是接收一个当前类右值引用对象的构造函数。

class Str{

public:
    Str() = default;
    Str(const Str & val) = default;
    Str(Str&& x)					// 移动构造函数
        :val(std::move(x.val))
    {
        
    }

private:
    std::string val;
};
           

关于移动构造函数,我们需要注意以下几点

  • 移动构造函数可以从输入对象中“偷窃”资源,只要确保在偷窃资源之后输入对象处于合法即可
  • 当某些特殊成员函数(如拷贝构造)未定义时,编译器可以生成一个缺省移动构造函数。缺省移动构造函数的行为是对数据成员中有移动构造函数的成员调用对应的移动构造函数,没有移动构造函数的成员调用对应的拷贝构造函数
  • 移动构造函数通常声明未不可抛出异常的函数(

    noexcept

    ),这样可以避免引入异常处理逻辑,提升性能。同时,一些可以选择调用移动构造和拷贝构造的逻辑只有在移动构造是

    noexcept

    时才会选择使用移动构造进行加速,这样主要是考虑了异常安全的问题。
  • 注意右值引用对象做表达式时是左值

继续阅读