• 【C++】构造函数、析构函数、拷贝构造函数


    目录

    构造函数 

    析构函数

    拷贝构造函数 


    C++ 在 C 语言的基础上增加了面向对象编程,C++ 支持面向对象程序设计。类是 C++ 的核心特性,通常被称为用户定义的类型。

    类用于指定对象的形式,它包含了数据表示法和用于处理数据的方法。类中的数据和方法称为类的成员。函数在一个类中被称为类的成员。

    类定义是以关键字 class 开头,后跟类的名称。类的主体是包含在一对花括号中。类定义后必须跟着一个分号或一个声明列表。例如,我们使用关键字 class 定义 Box 数据类型,如下所示:

    class Box

    {

    public:

            double getVolume(void)

            {

                    return length * breadth * height;

            }

    private:

            double length; // 盒子的长度

            double breadth; // 盒子的宽度

            double height; // 盒子的高度

    };

    类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。

    定义 C++ 对象

    Box Box1; // 声明 Box1,类型为 Box

    Box Box2; // 声明 Box2,类型为 Box

     可以在类的外部使用范围解析运算符 :: 定义该函数

    double Box::getVolume(void)

    {

            return length * breadth * height;

    }

     在 :: 运算符之前必须使用类名。调用成员函数是在对象上使用点运算符(.),这样它就能操作与该对象相关的数据:

    Box myBox; // 创建一个对象

    myBox.getVolume(); // 调用该对象的成员函数

    构造函数 

    类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。

    构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。

    构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任 务并不是开空间创建对象,而是初始化对象。

    1. 函数名与类名相同。

    2. 无返回值。

    3. 对象实例化时编译器自动调用对应的构造函数。

    4. 构造函数可以重载。

    无参的构造函数 

    1. #include
    2. using namespace std;
    3. class Line
    4. {
    5. public:
    6. void setLength( double len );
    7. double getLength( void );
    8. Line(); // 这是构造函数
    9. private:
    10. double length;
    11. };
    12. // 成员函数定义,包括构造函数
    13. Line::Line(void)
    14. {
    15. cout << "Object is being created" << endl;
    16. }
    17. void Line::setLength( double len )
    18. {
    19. length = len;
    20. }
    21. double Line::getLength( void )
    22. {
    23. return length;
    24. }
    25. // 程序的主函数
    26. int main( )
    27. {
    28. Line line;
    29. // 设置长度
    30. line.setLength(6.0);
    31. cout << "Length of line : " << line.getLength() <
    32. return 0;
    33. }

     带参的构造函数

    1. #include
    2. using namespace std;
    3. class Line
    4. {
    5. public:
    6. void setLength( double len );
    7. double getLength( void );
    8. Line(double len); // 这是构造函数
    9. private:
    10. double length;
    11. };
    12. // 成员函数定义,包括构造函数
    13. Line::Line( double len)
    14. {
    15. cout << "Object is being created, length = " << len << endl;
    16. length = len;
    17. }
    18. void Line::setLength( double len )
    19. {
    20. length = len;
    21. }
    22. double Line::getLength( void )
    23. {
    24. return length;
    25. }
    26. // 程序的主函数
    27. int main( )
    28. {
    29. Line line(10.0);
    30. // 获取默认设置的长度
    31. cout << "Length of line : " << line.getLength() <
    32. // 再次设置长度
    33. line.setLength(6.0);
    34. cout << "Length of line : " << line.getLength() <
    35. return 0;
    36. }

    如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。 

    class Line
    {
    public:
        // 这是构造函数
        /*Line(double len)
        {
            cout << "Object is being created, length = " << len << endl;
            length = len;
        }*/

    private:
        double length;
    };

    int main()
    {
        Line line;
        // 将Line类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
            // 将Line类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成
            // 无参构造函数,放开后报错:error C2512: “Line”: 没有合适的默认构造函数可用
        // 获取默认设置的长度

        return 0;
    }

    无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。(构成函数重载,但是调用存在歧义) 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。

    使用初始化列表来初始化字段 

    初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟 一个放在括号中的初始值或表达式。

    Line::Line( double len): length(len) {

    cout << "Object is being created, length = " << len << endl;

    }

    多个字段需要初始化

    C::C( double a, double b, double c): X(a), Y(b), Z(c) { .... } 

    成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

    class A {

    public:  

             A(int a)  :_a1(a) ,_a2(_a1)  

                    {}   

    private:

           int _a2;    

           int _a1;

    };

    int main() {    

            A aa(1);//先初始a2,再初始化a1

    }

    注意 

    1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

    2. 类中包含以下成员,必须放在初始化列表位置进行初始化:

            引用成员变量

            const成员变量

            自定义类型成员(且该类没有默认构造函数时) 

    3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量, 一定会先使用初始化列表初始化。

    4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

              class A

            {

            public:

              A(int a)

                :_a1(a)

                ,_a2(_a1)

                  {} 

              void Print()

              {

                cout<<_a1<<" "<<_a2<

              }

            private:

              int _a2;

              int _a1;

            }

            int main()

            {

              A aa(1);

              aa.Print();// _a1=1,_a2为随机值

            //初始化顺序由定义类时的声明顺序决定,所以先初始化_a2,由于初始化_a2时_a1还未初始化,所以为随机值

            }

    析构函数

    类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。

    与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

    析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。

    当我们用C语言写栈时,要对栈进行初始化和销毁,在写栈的时候我们可能会忘记把栈销毁,如果不对栈进行销毁,可能会造成内存泄漏,这是很危险的,但当我们学会C++的析构函数之后,析构函数就会自动调用,销毁栈,用起来就会很方便。

    1. typedef int DataType;
    2. class Stack
    3. {
    4. public:
    5. Stack(size_t capacity = 3)
    6. {
    7. _array = (DataType*)malloc(sizeof(DataType) * capacity);
    8. if (NULL == _array)
    9. {
    10. perror("malloc申请空间失败!!!");
    11. return;
    12. }
    13. _capacity = capacity;
    14. _size = 0;
    15. }
    16. void Push(DataType data)
    17. {
    18. // CheckCapacity();
    19. _array[_size] = data;
    20. _size++;
    21. }
    22. // 其他方法...
    23. //栈的析构函数
    24. ~Stack()
    25. {
    26. if (_array)
    27. {
    28. free(_array);
    29. _array = NULL;
    30. _capacity = 0;
    31. _size = 0;
    32. }
    33. }
    34. private:
    35. DataType* _array;
    36. int _capacity;
    37. int _size;
    38. };
    39. void TestStack()
    40. {
    41. Stack s;
    42. s.Push(1);
    43. s.Push(2);
    44. }

    编译器生成的默认析构函数,对自定类型成员调用它的析构函数。

    1. class Time
    2. {
    3. public:
    4. ~Time()
    5. {
    6. cout << "~Time()" << endl;
    7. }
    8. private:
    9. int _hour;
    10. int _minute;
    11. int _second;
    12. };
    13. class Date
    14. {
    15. private:
    16. // 基本类型(内置类型)
    17. int _year = 1970;
    18. int _month = 1;
    19. int _day = 1;
    20. // 自定义类型
    21. Time _t;
    22. };
    23. int main()
    24. {
    25. Date d;
    26. return 0;
    27. }

     在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?          main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month, _day三个是 内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date 类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁 ,main函数中并没有直接调用Time类的析构函数,而是显式调用编译器为Date类生成的默认析构函数。

    如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。

    拷贝构造函数 

    只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

    拷贝函数通常用于:

    • 通过使用另一个同类型的对象来初始化新创建的对象。

    • 复制对象把它作为参数传递给函数。

    • 复制对象,并从函数返回这个对象。

    拷贝函数的形式:

    classname ( const classname &obj) {

             // 构造函数的主体

    写拷贝构造函数最好加上const,防止误用,把传过来的参数修改了。 

    拷贝构造函数是构造函数的一个重载形式。

    拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。 

    如果类中没有显式定义拷贝构造函数,则C++编译器会自动生成一个无参的默认拷贝构造函数,在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。

    class Date {

    public:

             Date(int year = 1900, int month = 1, int day = 1) {

                     _year = year;

                    _month = month;

                     _day = day;

            }

              // Date(const Date d)  // 错误写法:编译报错,会引发无穷递归

            // 正确写法  

            Date(const Date& d)  

            {

            _year = d._year;

             _month = d._month;

            _day = d._day;

            }

    private:

            int _year;

            int _month;

            int _day;

    };

    int main() {

    Date d1;

    Date d2(d1);

    return 0;

    }

    若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。 

    class Time {

    public:

            Time() {

                    _hour = 1;

                    _minute = 1;

                    _second = 1;

    }

            Time(const Time& t) {

                    _hour = t._hour;

                     _minute = t._minute;

                     _second = t._second;

                    cout << "Time::Time(const Time&)" << endl;

               }

    private:

            int _hour;

            int _minute;

             int _second;

    };

    class Date {

            private: // 基本类型(内置类型)

            int _year = 1970;

            int _month = 1;

            int _day = 1; // 自定义类型

            Time _t;

            };

    int main() {

            Date d1;        // 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数,但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数         Date d2(d1);

             return 0; }

    像Date类可以使用默认的拷贝构造函数,因为只有值的拷贝,是浅拷贝。

    如果要实现栈的拷贝,栈的拷贝是深拷贝,需要完成深拷贝的构造函数。因为栈的内存是动态开辟出来的,浅拷贝只能拷贝内存的地址,并不能拷贝内存里的值,所以像这种情况不能使用默认的拷贝构造函数,需要自己定义拷贝构造函数。

    st1和st2指向同一块空间,插入删除数据时会被同时修改,造成数据混乱;调用析构函数时,同一块空间会被析构两次,造成程序崩溃。 

    void Push(const DataType& data) {

            // CheckCapacity();

             _array[_size] = data;

            _size++;

    }

    类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。 

    为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。

  • 相关阅读:
    引用计数法
    不安装运行时运行 .NET 程序 - NativeAOT
    Python写猜数游戏
    .NET Core 使用 System.Threading.Channels消息队列
    idea2023和历史版本的下载
    vscode debug python launch.json添加args不起作用
    听GPT 讲Istio源代码--pilot
    趣学python编程 (五、常用IDE环境推荐)
    一文理解Cookie、Session
    QLineEdit 使用QValidator 限制各种输入
  • 原文地址:https://blog.csdn.net/m0_55752775/article/details/127886971