掘金 阅读 ( ) • 2024-04-03 16:49

构造函数的概念与特点

C++构造函数和Java的构造函数是类似的。当创建一个对象时,构造函数会被自动调用。可以在创建对象时初始化对象的成员变量。构造函数具有如下特点:

  • 构造函数的名称与类名称相同
  • 没有返回值
  • 可以有参数,也可以没参数
  • 如果没有定义构造函数,则编译器会提供一个默认的无参构造函数
  • 构造函数可以重载

在下面的demo中申明了一个无参构造,一个有参构造函数。注意:这里有个最佳实践的技巧,c++不需要每个成员都声明其访问修饰符,类成员的访问修饰符( publicprivateprotected )通常是写在一起的,并且修饰符只需要在第一个成员前面进行声明。这样可以提高代码的可读性和简洁性。这里只是建议大家不要在c++中写出java风格的代码。

class Student {
public:
    Student() {} //无参构造函数
    Student(string name, int age); //有参构造函数

private:
    string _name; //c++中私有成员变量建议_开头,这是一种规范,提高代码可读性
    int _age;
};

注意,上面只是类的申明,下面才是类的定义。在实际的c++项目开发中,类的申明和定义一般是分开的,类的申明是在头文件.h中,定义(也就是具体的实现)在源文件.cpp中。 这里先有个概念即可,后面会有一节专门来讲这个。

初始化成员变量

//类的有参构造函数的定义
Student::Student(string name, int age) {
    this->_name = name;
    this->_age = age;
}

这里是初始化了2个成员变量。我们在Java中很多时候就是用的这种方式来初始化成员变量,但是在C++中不建议这种方式,而是提供了一种初始化成员列表的方式。

//初始化成员列表的方式
Student::Student(string name, int age): _name(name),_age(age) {
    cout<<"name: "<<_name<<endl;
    cout<<"age: "<<_age<<endl;
}

有以下几种情况是要求必须在初始化成员列表中初始化的

  • const成员变量
  • 引用类型成员变量
  • 类对象

创建对象的几种方式

int main() {
    //推荐方式
    Studen stu;
    Studen stu2("zx", 18);

    Studen stu3 = Studen(); //不建议
    //new是运算符,不是关键字,用于堆内存创建对象,注意,需要手动释放指针
    Studen *stu4 = new Studen("lisi",20);
    return 0;
}

在C++中, new 是一个运算符,用于在堆上动态分配内存来创建对象。它返回一个指向新分配对象的指针。常规的对象创建不建议用这种方式,因为使用复杂,还需要手动释放指针。

析构函数概念与使用

C++中的析构函数是一种特殊的成员函数,用于在对象被销毁时执行清理操作。

析构函数具有如下特点:

  • 析构函数的名称与类名相同,但前面加上一个波浪号~作为前缀。
  • 析构函数没有返回值,也不接受任何参数。
  • 自动调用
  • 如果类中没有显式定义析构函数,编译器会自动生成一个默认的析构函数。

析构函数在以下情况下被自动调用:

  1. 当对象的生命周期结束时,即对象超出其作用域。
  2. 当通过delete运算符显式销毁动态分配的对象。
  3. 当对象是局部对象并且位于函数的栈帧中,在函数执行完毕后会自动调用析构函数。
class Studen {
public:
    Studen(string name, int age) : _name(name), _age(age) {
        //堆内存申请空间,指针指向该地址
        arr = new char[10];
    };

    ~Studen() {
        //释放指针
        delete[] arr;
    }

private:
    string _name;
    int _age;
    char *arr;
};

析构函数的主要作用是释放对象所占用的资源,例如释放动态分配的内存、关闭文件、释放锁等。需要注意的是,如果类中有指针成员变量或资源需要手动释放,就需要显式定义析构函数来执行相应的清理操作。在析构函数中,可以使用delete运算符释放动态分配的内存,或者调用其他适当的清理函数来释放资源。

拷贝构造函数

拷贝构造函数是一种特殊的构造函数。接下来探讨这几个问题

  • 拷贝构造函数是什么?怎么用?
  • 拷贝构造函数触发时机?使用场景?
  • 浅拷贝与深拷贝的区别?
  • 深拷贝如何实现?
  • 一些最佳实践的经验

下面的Demo定义了User类,类中定义了无参构造函数,有参构造函数,析构函数,以及2个成员变量。

class User {
public:
    User() {
        cout << "User()" << endl;
    };

    User(long uid, int sex) : uid(uid), sex(sex) {
        cout << " User(long uid,int sex)" << endl;
    }

    ~User() {
        cout << " ~User()" << endl;
    }

    long uid;
    int sex;
};

main方法

int main() {
    User user1(100L, 1);
    User user2 = user1;

    cout << "user2 uid: " << user2.uid << endl;
    cout << "user2 sex: " << user2.sex << endl;
    cout << &user1 << endl;
    cout << &user2 << endl;

    return 0;
}
}

这里先初始化了一个对象user1,然后通过创建好的user1去初始化一个对象user2。然后就是一些打印输出,我们先来看下结果:

输出如下
User(long uid,int sex)
user2 uid: 100
user2 sex: 1
0x7ff7bc5d77f8
0x7ff7bc5d77e0
~User()
~User()

这里创建user2对象的时候,看上去和java创建方式是不是很像? 而且从结果上看user2的变量的值和user1是一样的。但是我们通过&取址运算符,发现2个对象指向的内存地址不一样。 在java中,User user2 = user1,user2是个引用,和user1指向的确实是同一个内存地址,所以操作的是同一个对象。但是c++中,这种方式是创建了一个新的对象,只是这个新对象的成员变量的值和已经初始化对象的值一样。

这是啥原因呢?这其实是通过User user2 = user1方式创建新对象的时候,c++编译器会为我们自动生成一个默认的拷贝构造函数。这个默认的拷贝构造函数执行的是浅拷贝,即仅仅将对象的成员变量逐个地复制到新对象中。

下面一起来看下拷贝构造函数是怎样的?

class User {
public:
    User() {
        cout << "User()" << endl;
    };

    User(long uid, int sex) : uid(uid), sex(sex) {
        cout << " User(long uid,int sex)" << endl;
    }

    //拷贝构造函数  
    User(const User &user) {
        cout << " User(const User& user)" << endl;
        cout << " User(const User& user) user uid: " << user.uid << endl;
        cout << " User(const User& user) user sex: " << user.sex << endl;
    }

    ~User() {
        cout << " ~User()" << endl;
    }

    long uid;
    int sex;
};

当这样User user2 = user1创建user2时,会自动调用拷贝构造函数。注意:只会调用拷贝构造,不会调用任何其他的构造函数。

User(const User& user)
User(const User& user) user uid: 100
User(const User& user) user sex: 1

这里是自己实现了拷贝构造函数,如果不写,编译器会自动生成一个拷贝构造函数,自动生成的代码大致如下:

//拷贝构造函数
User(const User &user) {  
//自动为每一个成员变量赋值
   uid = user.uid;  
   sex = user.sex;  
}

拷贝构造函数的语法(必须这样)

  • 与无参构造,有参构造是一样的,区别在于参数不同
  • 参数只有一个,是当前类的引用
  • const是为了避免方法内部通过引用去修改对象的属性

上面的调用,相当于user2 调用User(user1)把user1传给拷贝构造函数;由于user1和引用类型user是指向的同一个对象,如果不加const,则可能在拷贝构造函数内部修改user1的值,所以为了避免这种风险需要强制加const。

一个新手误区

如果我们自己实现了如下的拷贝构造函数,是有问题的。你会发现user引用是有值的,因为拷贝构造是被调用了,但是User user2 = user1这里就不会把user1的变量(uid,sex)的值自动拷贝给user2了。所以说重写拷贝构造函数 的时候,需要手动赋值。

//拷贝构造函数 
User(const User& user){ 
    cout << " User(const User& user)" << endl; 
    cout << " User(const User& user) user uid: "<<user.uid << endl; 
    cout << " User(const User& user) user sex: "<<user.sex << endl; 
}

浅拷贝与深拷贝

在上面例子User user2 = user1已经说明编译器会自动创建并调用拷贝构造函数,就像调用默认的无参构造一样,而且不需要我们手动挨个给对象的成员变量赋值,这就是浅拷贝。

关于深拷贝,先不下定义,我们先把上面demo修改下,新增了一个变量address,是个数组指针,然后在有参构造函数中通过new运算符在堆内存上开辟了8 byte 的空间,在析构函数中释放指针。

class User {
public:
    User() {
        cout << "User()" << endl;
    };

    User(long uid, int sex) : uid(uid), sex(sex) {
        cout << " User(long uid,int sex)" << endl;
        address = new int[2];
        address[0] = 100;
        address[1] = 101;
    }


    ~User() {
        cout << " ~User()" << endl;
        delete[] address;
    }

    void printAddress(){
        for (int i = 0; i < 2; ++i) {
            cout << address[i] << " ";
        }
        cout << endl;
    }

    long uid;
    int sex;
    int *address; //存储一堆地址的数组
};

main方法

int main() {
    User user1(100L, 1);  
    User user2 = user1; 
    cout << "user1 uid: " << user1.uid << endl;  
    cout << "user2 uid: " << user2.uid << endl;  
  
    cout << "user1 address: " << user1.address << endl;  
    cout << "user2 address: " << user2.address << endl;  
  
    cout << "user1 address value:";  
    user1.printAddress();  
    cout << "user2 address value:";  
    user2.printAddress();  
  
    return 0;
}

看下打印结果 image.png

可以发现user1和user2中 ,指针变量address无论是内存地址还是数组中的值都是相同的。这是因为编译器自动生成的拷贝构造中,会自动为指针变量赋值,大致代码如下:

User(const User &user) {  
    this->address = user.address;  
}

但是细心的你已经发现这里抛出了异常,而且main方法也不是return 0了(这是正常情况)。这个异常是说,要释放的指针还没有分配内存。

异常原因分析

因为main方法执行完之后,User对象就会销毁,就会执行析构函数,这里面会通过delete运算符释放指针。从打印结果可以看到2个对象的析构函数都执行了,user1先释放了指针,user2再去释放指针时,由于2个对象的指针是指向的的同一个内存地址,所以相当于user2再去delete的时候这个指针已经被释放了,这个操作是不被允许的,所以抛出了异常。

从上面的demo和分析可以看出,当类中有指针变量的时候,浅拷贝是存在问题的。这时候就需要深拷贝了,深拷贝的实现规则如下:

  • 需要我们自己重写这个拷贝构造函数;
  • 通过引用对成员变量赋值;
  • 成员变量是指针,要指向一个新的地址(分配新的内存空间),不能通过引用去赋值;
  • 指针变量指向的内存的值拷贝。

深拷贝关键代码如下:

 User(const User &user) {
        cout << " User(const User& user)" << endl;
        uid = user.uid;
        sex = user.sex;
        //指向一个新的地址
        address = new int[2];
        //值的拷贝,sizeof是获取内存大小
        std::memcpy(address, user.address, 2 * sizeof(int));
    }

一些实践技巧

还是使用之前的User类,下面定义一个方法,方法的形参是User对象

void func(User user){ }

//main
int main() {
   User user1(100L, 1);
   func(user1);
}

//输出
 User(long uid,int sex)
 User(const User& user)
 ~User()
 ~User()

当函数形参是类对象的时候,也会调用拷贝构造函数,在方法执行完之后会调用析构函数。 因为本质上还是用一个已经创建的对象初始化一个新对象,所以这种方式会产生临时对象。可以通过引用来优化,代码如下:

void func(const User &user){ }

c++中建议使用引用作为函数的形参

总结

1、拷贝构造函数调用时机:当使用一个创建好的对象去初始化一个新的对象时,这个新对象的创建就会调用拷贝构造函数

2、一定注意拷贝构造函数的固定写法,只有一个connst 的引用作为参数;

3、默认是浅拷贝,浅拷贝会自动为所有变量赋值;如果类中有指针,就需要用深拷贝了,而深拷贝需要自己实现。