C++ 构造函数、析构函数、拷贝构造函数
发布日期:2021-05-07 15:11:15 浏览次数:21 分类:精选文章

本文共 7484 字,大约阅读时间需要 24 分钟。

原文链接:

一、构造函数

概念(作用):

  • 用来在创建对象时,对对象进行初始化

 

特点

  • 构造函数无返回值,函数名与类名相同,必须存在于public中
  • 构造函数可以重载
  • 不用来初始化static数据成员,因为static数据成员不属于类

默认构造函数(合成的构造函数)

  • 如果没有在类中给出构造函数,编译器自动生成一个默认的构造函数(无参数、函数体为空)
  • 当在类中手动给出构造函数时,默认的构造函数消失
  • 编译器自动生成的默认的构造函数又称为合成的默认构造函数
  • 默认构造函数做了什么:

 

  1. 如果类成员变量有初始值,用这些初始值初始化成员变量
  2. 如果类成员变量没有初始值,则默认初始化该成员变量
class Student {public:    int age = 18;    long id = 217060;    std::string name;}; int main(){    /*执行默认构造函数    age、id、name分别被初始化为18、217060、空字符串    */    Student stu;    return 0;}

使用注意事项:

  • 当类中存在内置类型或复合类型(比如数组和指针)或管理动态内存的类时,必须手动定义构造函数,不能使用默认的构造函数。否则,用户创建的类的对象就会得到未定义的值

default构造函数(C++11标准)

 

  • 设计目的:我们知道当一个类中手动给出构造函数时,默认的构造函数就会消失。C++11标准定义,如果当类中给出构造函数时,我们还需要类的默认构造函数,那么就可以在构造函数后面给出“=default”,这样这个构造函数的作用就等同与默认构造函数

 

语法:

  • =default既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部
  • 和其它函数一样,如果=default在类的内部,则默认构造函数是内联的;如果它在类的外部,则该成员默认情况下不是内联的
  • 重点:=default的默认构造函数必须遵守默认构造函数的规则,如果你的编译器不支持类内初始值,那么你的默认构造函数就应该使用构造函数“初始值列表”来初始化类的每个成员(见下面的案例)、
//这个默认构造函数之所以对Student有效,是因为我们为内置类型的数据成员提供了初始值//如果你的编译器不支持类内初始值,那么你的默认构造函数就应该使用构造函数初始值列表来初始化类的每个成员class Student {public:    Student()=default;    Student(int age, int id) {    this->age = age;        this->id = id;    }public:	int age = 18;	long id = 217060;	std::string name;}; int main(){	Student stu; //相当于执行了“Student()=default”构造函数,也相当于执行了类的默认构造函数	return 0;}

 

  • 演示案例
class Cat{private:    int m_Feet;    int m_Ear;pubic:    Cat();    Cat(int a,int b);}; int main(){    Cat oCat();//可能会出现错误,被误系统认为一个函数        Cat oCat;//调用无参构造    Cat oCat(1,2);//调用有参构造    Cat oCat=Cat();//调用无参构造    Cat oCat=Cat(1,2);//调用有参构造     Cat* pCat=new Cat;//调用无参构造    Cat* pCat=new Cat();//调用无参构造    Cat* pCat=new Cat(1,2);//调用有参构造}

注意事项

  • ①在上面的实例中 Cat oCat=Cat(); 其实为拷贝构造:即=号后面构造一个临时对象,然后拷贝给oCat,拷贝构造完成后,临时对象析构消失
  • ②在上面的实例中 Cat oCat();可能会被系统误认为一个函数(返回值类型为Cat,函数名为oCat),因为我们建议在调用无参构造函数时,不建议加上括号。请看下面代码、
Cat oCat();//不安全的初始化,可能会被误认为声明一个函数Cat oCat; //安全的初始化

 

二、析构函数

概念(作用):

  • 用来对对象进行善后处理(指针释放、内存释放等...)

特点:

  • 析构函数无返回值,函数名:~类名,必须存在于public中
  • 析构函数只有一个,不能重载
  • 析构函数不可以主动调用,等到对象被释放时,系统自动执行析构函数
  • 与构造函数,析构函数不用来销毁static数据成员
  • 与构造函数相反,构造函数初始化数据成员的顺序是按照数据成员在类中出现的顺序进行初始化的;析构函数执行时,按照成员初始化顺序的逆序销毁

什么时候调用析构函数:

无论何时一个对象被销毁,就会自动调用其析构函数:

  • 变量在离开其作用域时被销毁。
  • 当一个对象被销毁时,其成员被销毁。
  • 容器(无论是标准库容器还是数组)被销毁时,其元素被销毁。
  • 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁
  • 对于临时对象,当创建它的完整表达式结束时被销毁。
{    Sales_data *p=new Sales_data;    auto p2=make_shared
(); Sales_data item(*p); vector
vec; vec.push_back(*p2); delete p;}//退出局部作用域时:对item、p2、vec调用析构函数//销毁p2会递减其引用计数;如果其引用计数变为0,对象被释放

 

默认析构函数(合成析构函数)

  • 如果没有在类中给出析构函数,编译器自动生成一个默认的析构函数(无参数、函数体为空)
  • 当手动给出析构函数时,默认的消失
  • 当类中有动态内存的变量时,不能使用默认析构函数,必须手动设计析构函数
  • 对于某些类,合成析构函数被用来阻止该类型的对象被销毁(见阻止阻止拷贝部分);如果不是这种情况,合成析构函数的函数体为空
class Sales_data{public:    ~Sales_data(){}    int a;    string s;}//Sales_data类定义的对象被销毁时,成员会被自动销毁,其中string的析构函数会被调用
  • 智能指针成员的销毁:

 

  • 与普通指针不同,智能指针数据成员是类类型,所以自己具有析构函数。因此智能指针成员在析构阶段会自动销毁,不需要手动销毁

使用=default

 

  • 与构造函数使用=default一样,析构函数也可以使用=default
  • =default的析构函数就相当于系统默认的析构函数
  • 当在类内使用=default时,函数将隐式地声明为内联,如果不希望是内联函数,就将函数在类外定义
class Sales_data{public:    ~Sales_data()=default;}
  • 我们只能对具有合成版本的成员函数使用=default(即,默认构造函数或拷贝控制成员)。

演示案例

class Cat{private:    char* name;public:    ~Cat();};Cat::~Cat(){    if(name)//先判断成员是否为空    {        delete[] name;        name=nullptr;    }}

 

  • 注意事项:释放带有内存的成员时,需要先判断成员变量是否为空。如果不为空则释放,如果为空则不释放

需要析构函数的类也需要拷贝和赋值操作

 

  • 原则:通常,如果一个类需要一个析构函数,我们几乎可以肯定这个类也需要一个拷贝构造函数和一个拷贝赋值运算符
  • 案例:
    • 下面有一个类和一个函数:函数是传值参数,因此参数会拷贝
class HasPtr{public:    HasPtr(const std::string &s=std::string()):ps(new std::string(s)),i(0){}    ~HasPtr(){delete ps;}private:    std::string *ps;    int i;} HasPtr f(HasPtr hp){    HasPtr ret=hp; //拷贝给定的HasPtr    return ret;  //ret和hp被销毁}
  • 当f返回时,hp和ret都被销毁,在两个对象上都会调用HasPtr的析构函数。此析构函数会delete ret和hp中的指针成员。但这两个对象包含相同的指针值。此代码会导致此指针被 delete两次,这显然是一个错误。将要发生什么是未定义的。
  • 此外,f的调用者还会使用传递给f的对象:
HasPtr p("some values");f(p);//当f结束时,p.ps指向的内存被释放HasPtr  q(p);//现在p和q都指向无效内存!
  • p(以及q)指向的内存不再有效,在hp(或ret!)销毁时它就被归还给系统了。

需要拷贝操作的类也需要赋值操作,反之亦然

  • 虽然很多类需要定义所有(或是不需要定义任何)拷贝控制成员,但某些类所要完成的工作,只需要拷贝或赋值操作,不需要析构函数。
  • 作为一个例子,考虑一个类为每个对象分配一个独有的、唯一的序号。这个类需要一个拷贝构造函数为每个新创建的对象生成一个新的、独一无二的序号。除此之外,这个拷贝构造函数从给定对象拷贝所有其他数据成员。这个类还需要自定义拷贝赋值运算符来避免将序号赋予目的对象。但是,这个类不需要自定义析构函数。
  • 这个例子引出了第二个基本原则:如果一个类需要一个拷贝构造函数,几乎可以肯定它也需要一个拷贝赋值运算符。反之亦然一一如果一个类需要一个拷贝赋值运算符,几乎可以肯定它也需要一个拷贝构造函数。然而,无论是需要拷贝构造函数还是需要拷贝赋值运算符都不必然意味着也需要析构函数。

拷贝构造函数

  • 使用一个已经存在的对象,去构造(初始化)另一个对象
  • 参数加上const&,因为拷贝构造函数在几种情况下都会被隐式地使用,因此拷贝构造函数不应该是explict的
  1. const:防止函数内部修改值
  2. &:防止无限循环拷贝
类名(类名 const& 参数名){    函数体}

 

拷贝构造函数的分类

 

  • 浅拷贝:成员变量无动态内存(指针等)变量时,在拷贝构造函数内对成员变量只做简单的赋值,不做内存申请
  • 深拷贝:成员变量有动态内存(指针等)变量时,在拷贝构造函数内对成员变量先进行内存申请,然后进行内容拷贝
  • 默认拷贝构造:没有写拷贝构造时,系统默认给出(默认的为浅拷贝)
     

浅拷贝:

//此类情况使用浅拷贝class Cperson{private:    int a;public:    Cperson(Cperson const& other);//拷贝构造}Cperson::Cperson(Cperson const& other){    this->a=other.a;}

深拷贝:

//此类含有指针的情况使用深拷贝class Cperson{private:    int age;    char* name;public:    Cperson(Cperson const& other);}Cperson::Cperson(Cperson const& other){    this->age=other->age;    if (other.name)//深拷贝	{            int len = strlen(other.name);            this->name = new char[len+1];            strcpy(this->name, other.name);	}	else            this->name = NULL;}int main(){    Cperson person1;    Cperson person2(person1);//隐式调用拷贝构造    Cperson person2=person1;//显示调用拷贝构造}

 

默认拷贝构造函数(合成拷贝构造函数)

  • 规则:如果没有主动给出拷贝构造,编译器会自动添加一个拷贝构造(做的是浅拷贝),主动给出后默认的拷贝构造消失
  • 如果类中有动态内存变量出现,必须重写拷贝构造,且使用深拷贝。如果没有动态内存变量出现,可不重写拷贝构造,用默认的即可

成员的数据类型决定其拷贝的规则

每个成员的类型绝对了它如何被拷贝:

  • 对类类型的成员,会使用其拷贝构造函数来拷贝
  • 内置类型的成员则直接拷贝
  • 虽然不能直接拷贝一个数组,但是合成拷贝构造函数会逐元素地拷贝一个数组类型的成员。如果数组元素是类类型,则使用元素的拷贝构造函数来进行拷贝

直接初始化和拷贝初始化

  • 直接初始化:实际上是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数
  • 拷贝初始化:要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换
string dots(10,'.');             //直接初始化string s(dots);                  //直接初始化string s2=dots;                  //直接初始化string null_book="9-999-99999-9";//拷贝初始化string nines=string(100,'9');    //拷贝初始化
  • 移动构造函数:拷贝初始化通常使用拷贝构造函数来完成。但是,如果一个类有一个移动构造函数,则拷贝初始化有时会使用移动构造函数而非拷贝构造函数来完成。但现在,我们只需了解拷贝初始化何时发生,以及拷贝初始化是依靠拷贝构造函数或移动构造函数来完成的就可以

拷贝构造出现的情景

  • 拷贝初始化不仅在我们使用=定义变量时会发生,在下列情况下也会发生
  1. 将一个对象作为实参传递给一个非引用类型的形参
  2. 从一个返回类型为非引用类型的函数返回一个对象
  3. 用花括号列表初始化一个数组中的元素或一个聚合类中的成员

使用=default

  • 与构造函数使用=default一样,拷贝构造函数也可以使用=default
  • =default的拷贝构造就相当于系统默认的拷贝构造
  • 当在类内使用=default时,函数将隐式地声明为内联,如果不希望是内联函数,就将函数在类外定义
class Sales_data{public:    Sales_data(const Sales_data&)=default;}

 

绕过拷贝构造函数

  • 在拷贝初始化过程中,编译器可以(但不是必须)跳过拷贝/移动构造函数,直接创建对象
  • 但是,即使编译器库绕过了拷贝/移动构造函数,但在这个程序点上,拷贝/移动构造函数必须是存在且可访问的(例如,不能是private的)
  • 例如:
string null_book="9-999-9999-9"; //拷贝初始化 改写为:string null_book("9-999-9999-9"); //编译器略过了拷贝构造函数

 

拷贝赋值运算符(=)

  • 赋值运算符的重载,返回值应该为运算符左侧对象的一个引用,否则返回的是一个临时对象
  • 如果没有写赋值重载运算符,编译器自动存在一个默认的,就是拷贝构造中所用到的默认拷贝构造,但是如果类成员变量中含有动态内存的变量时,需要重载赋值运算符
class Cperson{private:    char* name;public:    Cperson& operator=(Cperson const& other);};Cperson& Cperson::operator=(Cperson const& other){    if(this->name)//先释放原先的数据        delete[] name;    if(other)//如果传入的参数不为空    {        int len=strlen(other.name);        this->name=new char[len+1];        strcpy(this->name,other.name);    }    else//如果传入的参数为空    {        other.name=nullptr;    }};

 

需要析构函数的类也需要拷贝和赋值操作

  • 原则:通常,如果一个类需要一个析构函数,我们几乎可以肯定这个类也需要一个拷贝构造函数和一个拷贝赋值运算符

案例:

  • 下面有一个类和一个函数
  • 函数是传值参数,因此参数要拷贝
class HasPtr{public:    HasPtr(const std::string &s=std::string()):ps(new std::string(s)),i(0){}    ~HasPtr(){delete ps;}private:    std::string *ps;    int i;} HasPtr f(HasPtr hp){    HasPtr ret=hp; //拷贝给定的HasPtr    return ret;  //ret和hp被销毁}

 

浅拷贝错误演示

  • 原因:下列代码中,other->name指向一块内存,直接把other->name赋值给this->name,则两个变量都指向同一块内存,虽然不会出错,但是对不同的指针操作,会改变另一个指针的内容,非常的危险,建议使用上面的深拷贝
class Cperson{private:    int age;    char* name;public:    Cperson(Cperson const& other);}Cperson::Cperson(Cperson const& other){    this->age=other.age;    this->name=other.name;//错误做法}

 

上一篇:C++ 类静态成员(static)、类常量成员(const)、friend友元
下一篇:C++ 函数重载、缺省参数

发表评论

最新留言

第一次来,支持一个
[***.219.124.196]2025年04月14日 00时53分58秒